diff --git a/.github/workflows/label-issue-pull-request.yml b/.github/workflows/label-issue-pull-request.yml deleted file mode 100644 index e52bba36d63..00000000000 --- a/.github/workflows/label-issue-pull-request.yml +++ /dev/null @@ -1,24 +0,0 @@ -# Runs creation of Pull Requests -# If the PR destination branch is main, add a needs-qa label unless created by renovate[bot] ---- -name: Label Issue Pull Request - -on: - pull_request: - types: - - opened # Check when PR is opened - paths-ignore: - - .github/workflows/** # We don't need QA on workflow changes - branches: - - 'main' # We only want to check when PRs target main - -jobs: - add-needs-qa-label: - runs-on: ubuntu-latest - if: ${{ github.actor != 'renovate[bot]' }} - steps: - - name: Add label to pull request - uses: andymckay/labeler@e6c4322d0397f3240f0e7e30a33b5c5df2d39e90 # 1.0.4 - if: ${{ !github.event.pull_request.head.repo.fork }} - with: - add-labels: "needs-qa" diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 6e010d1b7ed..f7d20044743 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -74,6 +74,7 @@ jobs: args: > -Dsonar.organization=${{ github.repository_owner }} -Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }} - -Dsonar.test.inclusions=**/*.spec.ts -Dsonar.tests=. -Dsonar.sources=. + -Dsonar.test.inclusions=**/*.spec.ts + -Dsonar.exclusions=**/*.spec.ts diff --git a/apps/browser/package.json b/apps/browser/package.json index f7c577e7f7f..3c8eb50f387 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.7.0", + "version": "2024.7.1", "scripts": { "build": "cross-env MANIFEST_VERSION=3 webpack", "build:mv2": "webpack", diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 6aeaadd81a4..4eb6c139792 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -556,6 +556,18 @@ "security": { "message": "الأمان" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "لقد حدث خطأ ما" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "سجل كلمة المرور" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "تأكيد البريد الإلكتروني مطلوب" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "يجب عليك تأكيد بريدك الإلكتروني لاستخدام هذه الميزة. يمكنك تأكيد بريدك الإلكتروني في خزنة الويب." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index c9d68c88ebb..72490926acf 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Güvənlik" }, + "confirmMasterPassword": { + "message": "Ana parolu təsdiqlə" + }, + "masterPassword": { + "message": "Ana parol" + }, + "masterPassImportant": { + "message": "Unutsanız, ana parolunuz geri qaytarıla bilməz!" + }, + "masterPassHintLabel": { + "message": "Ana parol ipucusu" + }, "errorOccurred": { "message": "Bir xəta baş verdi" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "$TYPE$ - bax", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Parol tarixçəsi" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "E-poçt doğrulaması tələb olunur" }, + "emailVerifiedV2": { + "message": "E-poçt doğrulandı" + }, "emailVerificationRequiredDesc": { "message": "Bu özəlliyi istifadə etmək üçün e-poçtunuzu doğrulamalısınız. E-poçtunuzu veb anbarında doğrulaya bilərsiniz." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Parol silindi" }, - "unassignedItemsBannerNotice": { - "message": "Bildiriş: Təyin edilməyən təşkilat elementləri artıq Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Bildiriş: 16 May 2024-cü il tarixindən etibarən, təyin edilməyən təşkilat elementləri Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Bu elementləri görünən etmək üçün", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "bir kolleksiyaya təyin edin.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Avto-doldurma təklifləri" }, @@ -3493,13 +3503,13 @@ "message": "Qovluğu olmayan elementlər" }, "itemDetails": { - "message": "Item details" + "message": "Element detalları" }, "itemName": { - "message": "Item name" + "message": "Element adı" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "\"Yalnız baxma\" icazələrinə sahib kolleksiyaları silə bilməzsiniz: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3521,33 @@ "message": "Təşkilat deaktiv edildi" }, "owner": { - "message": "Owner" + "message": "Sahiblik" }, "selfOwnershipLabel": { - "message": "You", + "message": "Siz", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Deaktiv edilmiş təşkilatlardakı elementlərə müraciət edilə bilməz. Kömək üçün təşkilatınızın sahibi ilə əlaqə saxlayın." }, + "additionalInformation": { + "message": "Əlavə məlumat" + }, + "itemHistory": { + "message": "Element tarixçəsi" + }, + "lastEdited": { + "message": "Son düzəliş" + }, + "ownerYou": { + "message": "Sahiblik: Siz" + }, + "linked": { + "message": "Əlaqələndirildi" + }, + "copySuccessful": { + "message": "Uğurla kopyalandı" + }, "upload": { "message": "Yüklə" }, @@ -3559,16 +3587,37 @@ "filters": { "message": "Filtrlər" }, + "personalDetails": { + "message": "Şəxsi detallar" + }, + "identification": { + "message": "İdentifikasiya" + }, + "contactInfo": { + "message": "Əlaqə məlumatı" + }, + "downloadAttachment": { + "message": "$ITEMNAME$ - endir", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { - "message": "Card details" + "message": "Kart detalları" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "$BRAND$ detalları", "placeholders": { "brand": { "content": "$1", "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index d855d8447f2..b4fdffc479d 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Бяспека" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Адбылася памылка" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Гісторыя пароляў" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Патрабуецца праверка электроннай пошты" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Вы павінны праверыць свой адрас электроннай пошты, каб выкарыстоўваць гэту функцыю. Зрабіць гэта можна ў вэб-сховішчы." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 360d73d41d8..db1c266177b 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Сигурност" }, + "confirmMasterPassword": { + "message": "Потвърждаване на главната парола" + }, + "masterPassword": { + "message": "Главна парола" + }, + "masterPassImportant": { + "message": "Главната парола не може да бъде възстановена, ако я забравите!" + }, + "masterPassHintLabel": { + "message": "Подсказка за главната парола" + }, "errorOccurred": { "message": "Възникна грешка" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Преглед на $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Хронология на паролата" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Изисква се потвърждение на е-пощата" }, + "emailVerifiedV2": { + "message": "Е-пощата е потвърдена" + }, "emailVerificationRequiredDesc": { "message": "Трябва да потвърдите е-пощата си, за да използвате тази функционалност. Можете да го направите в уеб-трезора." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Секретният ключ е премахнат" }, - "unassignedItemsBannerNotice": { - "message": "Известие: неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори, а са достъпни само през Административната конзола." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Известие: след 16 май 2024, неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори, а ще бъдат достъпни само през Административната конзола." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Добавете тези елементи към колекция в", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "за да ги направите видими.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Автоматично попълване на предложения" }, @@ -3493,13 +3503,13 @@ "message": "Елементи без папка" }, "itemDetails": { - "message": "Подробности за елемент" + "message": "Подробности за елемента" }, "itemName": { - "message": "Име на елемент" + "message": "Име на елемента" }, "cannotRemoveViewOnlyCollections": { - "message": "Не можете да премахнете колекции с права „Само за преглед“: $COLLECTIONS$", + "message": "Не можете да премахвате колекции с права „Само за преглед“: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Записите в деактивирани организации не са достъпни. Свържете се със собственика на организацията си за помощ." }, + "additionalInformation": { + "message": "Допълнителна информация" + }, + "itemHistory": { + "message": "История на елемента" + }, + "lastEdited": { + "message": "Последна промяна" + }, + "ownerYou": { + "message": "Собственик: Вие" + }, + "linked": { + "message": "Свързано" + }, + "copySuccessful": { + "message": "Копирането е успешно" + }, "upload": { "message": "Качване" }, @@ -3559,16 +3587,37 @@ "filters": { "message": "Филтри" }, + "personalDetails": { + "message": "Лични данни" + }, + "identification": { + "message": "Идентификация" + }, + "contactInfo": { + "message": "Информация за връзка" + }, + "downloadAttachment": { + "message": "Сваляне – $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Данни за картата" }, "cardBrandDetails": { - "message": "$BRAND$ подробности", + "message": "Подробности за $BRAND$", "placeholders": { "brand": { "content": "$1", "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index cffb78f5b46..528248476d0 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -556,6 +556,18 @@ "security": { "message": "নিরাপত্তা" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "একটি ত্রুটি উৎপন্ন হয়েছে" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "পাসওয়ার্ড ইতিহাস" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "ইমেইল সত্যায়ন প্রয়োজন" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 48159dcd6d2..0040eb2a433 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 3d7ae128fbc..467673e6c91 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Seguretat" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "S'ha produït un error" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Historial de les contrasenyes" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Es requereix verificació del correu electrònic" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Heu de verificar el correu electrònic per utilitzar aquesta característica. Podeu verificar el vostre correu electrònic a la caixa forta web." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Clau de pas suprimida" }, - "unassignedItemsBannerNotice": { - "message": "Avís: els elements de l'organització no assignats ja no són visibles a la visualització de Totes les caixes fortes i només es poden accedir des de la Consola d'administració." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Avís: el 16 de maig de 2024, els elements de l'organització no assignats deixaran de ser visibles a la visualització de Totes les caixes fortes i només es podran accedir des de la Consola d'administració." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assigna aquests elements a una col·lecció de", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "per fer-los visibles.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 74c8ce12125..1067219e023 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Zabezpečení" }, + "confirmMasterPassword": { + "message": "Potvrzení hlavního hesla" + }, + "masterPassword": { + "message": "Hlavní heslo" + }, + "masterPassImportant": { + "message": "Pokud zapomenete Vaše hlavní heslo, nebude možné jej obnovit!" + }, + "masterPassHintLabel": { + "message": "Nápověda k hlavnímu heslu" + }, "errorOccurred": { "message": "Vyskytla se chyba" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Zobrazit $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Historie hesel" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Je vyžadováno ověření e-mailu" }, + "emailVerifiedV2": { + "message": "E-mail byl ověřen" + }, "emailVerificationRequiredDesc": { "message": "Abyste mohli tuto funkci používat, musíte ověřit svůj e-mail. Svůj e-mail můžete ověřit ve webovém trezoru." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Přístupový klíč byl odebrán" }, - "unassignedItemsBannerNotice": { - "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve vašem zobrazení všech trezorů a jsou nyní přístupné pouze v konzoli správce." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Upozornění: 16. květba 2024 již nebudou nepřiřazené položky organizace viditelné ve vašem zobrazení všech trezorů a budou přístupné pouze v konzoli správce." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Přiřadit tyto položky ke kolekci z", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "aby byly viditelné.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Návrhy automatického vyplňování" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "K položkám v deaktivované organizaci nemáte přístup. Požádejte o pomoc vlastníka organizace." }, + "additionalInformation": { + "message": "Další informace" + }, + "itemHistory": { + "message": "Historie položky" + }, + "lastEdited": { + "message": "Naposledy upraveno" + }, + "ownerYou": { + "message": "Vlastník: Vy" + }, + "linked": { + "message": "Propojeno" + }, + "copySuccessful": { + "message": "Kopírování bylo úspěšné" + }, "upload": { "message": "Nahrát" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filtry" }, + "personalDetails": { + "message": "Osobní údaje" + }, + "identification": { + "message": "Identifikace" + }, + "contactInfo": { + "message": "Kontaktní informace" + }, + "downloadAttachment": { + "message": "Stahování - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index ae3bb72c0b0..55c7b494e16 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Diogelwch" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Bu gwall" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Hanes cyfrineiriau" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index a8aeac4ef6e..5ff1553f895 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Sikkerhed" }, + "confirmMasterPassword": { + "message": "Bekræft hovedadgangskode" + }, + "masterPassword": { + "message": "Hovedadgangskode" + }, + "masterPassImportant": { + "message": "Hovedadgangskoden kan ikke gendannes, hvis den glemmes!" + }, + "masterPassHintLabel": { + "message": "Hovedadgangskodetip" + }, "errorOccurred": { "message": "Der er opstået en fejl" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Vis $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Adgangskodehistorik" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "E-mailbekræftelse kræves" }, + "emailVerifiedV2": { + "message": "E-mail bekræftet" + }, "emailVerificationRequiredDesc": { "message": "Du skal bekræfte din e-mail for at bruge denne funktion. Du kan bekræfte din e-mail i web-boksen." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Adgangsnøgle fjernet" }, - "unassignedItemsBannerNotice": { - "message": "Bemærk: Utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Bemærk: Pr. 16. maj 2024 er utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Tildel disse emner til en samling via", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "for at gøre dem synlige.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Autoudfyldningsforslag" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Emner i deaktiverede organisationer kan ikke tilgås. Kontakt organisationsejeren for assistance." }, + "additionalInformation": { + "message": "Yderligere oplysninger" + }, + "itemHistory": { + "message": "Emnehistorik" + }, + "lastEdited": { + "message": "Senest redigeret" + }, + "ownerYou": { + "message": "Ejer: Dig" + }, + "linked": { + "message": "Linket" + }, + "copySuccessful": { + "message": "Kopieret" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filtre" }, + "personalDetails": { + "message": "Personlige oplysninger" + }, + "identification": { + "message": "Identifikation" + }, + "contactInfo": { + "message": "Kontaktoplysninger" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Kortoplysninger" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 11900e883bd..444b2df0977 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Sicherheit" }, + "confirmMasterPassword": { + "message": "Master-Passwort bestätigen" + }, + "masterPassword": { + "message": "Master-Passwort" + }, + "masterPassImportant": { + "message": "Dein Master-Passwort kann nicht wiederhergestellt werden, wenn du es vergisst!" + }, + "masterPassHintLabel": { + "message": "Master-Passwort-Hinweis" + }, "errorOccurred": { "message": "Ein Fehler ist aufgetaucht" }, @@ -1106,17 +1118,17 @@ "message": "Authenticator App" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Gib einen Code ein, der von einer Authentifizierungs-App wie dem Bitwarden Authenticator generiert wurde.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP Security Key" + "message": "Yubico OTP-Sicherheitsschlüssel" }, "yubiKeyDesc": { "message": "Verwende einen YubiKey um auf dein Konto zuzugreifen. Funtioniert mit YubiKey 4, Nano 4, 4C und NEO Geräten." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Gib einen von Duo Security generierten Code ein.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1133,7 +1145,7 @@ "message": "E-Mail" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Gib einen an deine E-Mail-Adresse gesendeten Code ein." }, "selfHostedEnvironment": { "message": "Selbst gehostete Umgebung" @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "$TYPE$ ansehen", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Passwortverlauf" }, @@ -1809,7 +1830,7 @@ "message": "jederzeit." }, "byContinuingYouAgreeToThe": { - "message": "Indem Sie fortfahren, stimmen Sie unseren" + "message": "Indem du fortfährst, stimmst du den" }, "and": { "message": "und" @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "E-Mail-Verifizierung erforderlich" }, + "emailVerifiedV2": { + "message": "E-Mail-Adresse verifiziert" + }, "emailVerificationRequiredDesc": { "message": "Du musst deine E-Mail Adresse verifizieren, um diese Funktion nutzen zu können. Du kannst deine E-Mail im Web-Tresor verifizieren." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey entfernt" }, - "unassignedItemsBannerNotice": { - "message": "Hinweis: Nicht zugeordnete Organisationseinträge sind nicht mehr in der Ansicht aller Tresore sichtbar und nur über die Administrator-Konsole zugänglich." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Hinweis: Ab dem 16. Mai 2024 sind nicht zugewiesene Organisationselemente nicht mehr in der Ansicht aller Tresore sichtbar und nur über die Administrator-Konsole zugänglich." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Weise diese Einträge einer Sammlung aus der", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "zu, um sie sichtbar zu machen.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Vorschläge zum Auto-Ausfüllen" }, @@ -3493,13 +3503,13 @@ "message": "Einträge ohne Ordner" }, "itemDetails": { - "message": "Item details" + "message": "Eintrag-Details" }, "itemName": { - "message": "Item name" + "message": "Eintrags-Name" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Du kannst Sammlungen mit Leseberechtigung nicht entfernen: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3521,33 @@ "message": "Organisation ist deaktiviert" }, "owner": { - "message": "Owner" + "message": "Besitzer" }, "selfOwnershipLabel": { - "message": "You", + "message": "Du", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Auf Einträge in deaktivierten Organisationen kann nicht zugegriffen werden. Kontaktiere deinen Organisationseigentümer für Unterstützung." }, + "additionalInformation": { + "message": "Zusätzliche Informationen" + }, + "itemHistory": { + "message": "Eintrags-Verlauf" + }, + "lastEdited": { + "message": "Zuletzt bearbeitet" + }, + "ownerYou": { + "message": "Eigentümer: Du" + }, + "linked": { + "message": "Verknüpft" + }, + "copySuccessful": { + "message": "Erfolgreich kopiert" + }, "upload": { "message": "Hochladen" }, @@ -3530,7 +3558,7 @@ "message": "Die maximale Dateigröße beträgt 500 MB" }, "deleteAttachmentName": { - "message": "Datei $NAME$ löschen", + "message": "Anhang $NAME$ löschen", "placeholders": { "name": { "content": "$1", @@ -3548,27 +3576,48 @@ } }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Sind Sie sich sicher, dass Sie diesen Anhang dauerhaft löschen möchten?" + "message": "Bist du sicher, dass du diesen Anhang dauerhaft löschen möchtest?" }, "premium": { "message": "Premium" }, "freeOrgsCannotUseAttachments": { - "message": "Free organizations cannot use attachments" + "message": "Kostenlose Organisationen können Anhänge nicht verwenden" }, "filters": { "message": "Filter" }, + "personalDetails": { + "message": "Persönliche Details" + }, + "identification": { + "message": "Identifikation" + }, + "contactInfo": { + "message": "Kontaktinformationen" + }, + "downloadAttachment": { + "message": "Herunterladen - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { - "message": "Card details" + "message": "Kartendetails" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "$BRAND$ Details", "placeholders": { "brand": { "content": "$1", "example": "Visa" } } + }, + "addAccount": { + "message": "Konto hinzufügen" } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 765738bcade..e60f2b61d16 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Ασφάλεια" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Παρουσιάστηκε σφάλμα" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Ιστορικό Κωδικού" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Απαιτείται Επαλήθευση Email" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Πρέπει να επαληθεύσετε το email σας για να χρησιμοποιήσετε αυτή τη δυνατότητα. Μπορείτε να επαληθεύσετε το email σας στο web vault." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 22e0eba1b4f..b6968f1ff87 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -611,6 +611,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -2762,6 +2765,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -3045,6 +3056,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3358,20 +3372,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3544,12 +3544,6 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, - "itemDetails": { - "message": "Item details" - }, - "itemName": { - "message": "Item name" - }, "additionalInformation": { "message": "Additional information" }, @@ -3636,5 +3630,169 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "assign": { + "message": "Assign" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "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 auto-fill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing auto-fill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp":{ + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown":{ + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } } } diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 9160b95ed22..886c6082673 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organisation items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organisation items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organisations cannot be accessed. Contact your organisation owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index d2793d4bd4e..6db451bdb34 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email Verification Required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organisation items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organisation items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organisations cannot be accessed. Contact your organisation owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 85cf8230698..cad044f9921 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Seguridad" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Ha ocurrido un error" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Historial de contraseñas" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Verificación de correo electrónico requerida" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Debes verificar tu correo electrónico para usar esta función. Puedes verificar tu correo electrónico en la caja fuerte web." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Clave de acceso eliminada" }, - "unassignedItemsBannerNotice": { - "message": "Aviso: Los elementos de organización no asignados ya no son visibles en la vista de Todas las cajas fuertes y solo son accesibles a través de la Consola de Administrador." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Aviso: El 16 de mayo de 2024, los elementos de organización no asignados no serán visibles en la vista de Todas las cajas fuertes y solo serán accesibles a través de la Consola de Administrador." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Asignar estos elementos a una colección de", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "para hcerlos visibles.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Autocompletar sugerencias" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "No se puede acceder a los elementos de las organizaciones desactivadas. Ponte en contacto con el propietario de tu organización para obtener ayuda." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Subir" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filtros" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Añadir cuenta" } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 08f70aff2af..d6b7120294f 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -3,30 +3,30 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwardeni paroolihaldur", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Kodus, tööl ja teel - Bitwarden hoiustab imelihtsalt kõik su paroolid, pääsuvõtmed ja tundliku info", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Logi oma olemasolevasse kontosse sisse või loo uus konto." }, "createAccount": { - "message": "Loo konto" + "message": "Konto loomine" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Määra tugev parool" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" + "message": "Lõpeta konto loomine parooli luues" }, "login": { "message": "Logi sisse" }, "enterpriseSingleSignOn": { - "message": "Ettevõtte Single Sign-On" + "message": "Ettevõtte ühekordne sisselogimine" }, "cancel": { "message": "Tühista" @@ -50,7 +50,7 @@ "message": "Vihje võib abiks olla olukorras, kui oled ülemparooli unustanud." }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Kui sa unustad oma parooli, saad saata parooli vihje e-mailile.\n$CURRENT$/$MAXIMUM$ tähepiirang.", "placeholders": { "current": { "content": "$1", @@ -72,13 +72,13 @@ "message": "Kaart" }, "vault": { - "message": "Hoidla" + "message": "Seif" }, "myVault": { - "message": "Minu hoidla" + "message": "Minu seif" }, "allVaults": { - "message": "Kõik hoidlad" + "message": "Kõik seifid" }, "tools": { "message": "Tööriistad" @@ -111,10 +111,10 @@ "message": "Automaatne täitmine" }, "autoFillLogin": { - "message": "Täida konto andmed" + "message": "Täida andmed automaatselt" }, "autoFillCard": { - "message": "Täida kaardi andmed" + "message": "Täida automaatselt kaardi andmed" }, "autoFillIdentity": { "message": "Täida identiteet" @@ -126,7 +126,7 @@ "message": "Kopeeri kohandatud välja nimi" }, "noMatchingLogins": { - "message": "Sobivaid kontoandmeid ei leitud." + "message": "Sobivaid kontoandmeid ei leitud" }, "noCards": { "message": "Kaardid puuduvad" @@ -144,7 +144,7 @@ "message": "Lisa identiteet" }, "unlockVaultMenu": { - "message": "Lukusta hoidla lahti" + "message": "Ava hoidla" }, "loginToVaultMenu": { "message": "Logi hoidlasse sisse" @@ -156,7 +156,7 @@ "message": "Lisa konto andmed" }, "addItem": { - "message": "Lisa kirje" + "message": "Lisa ese" }, "passwordHint": { "message": "Parooli vihje" @@ -189,25 +189,25 @@ "message": "Muuda ülemparooli" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Jätka veebibrauseris?" }, "continueToWebAppDesc": { - "message": "Explore more features of your Bitwarden account on the web app." + "message": "Uuri teisi Bitwardeni konto funktsioone veebirakenduses." }, "continueToHelpCenter": { - "message": "Continue to Help Center?" + "message": "Kas soovid minna Abikeskusesse?" }, "continueToHelpCenterDesc": { - "message": "Learn more about how to use Bitwarden on the Help Center." + "message": "Uuri teisigi Bitwardeni kasutusvõimalusi Abikeskuses." }, "continueToBrowserExtensionStore": { - "message": "Continue to browser extension store?" + "message": "Mine edasi veebilaienduste poodi?" }, "continueToBrowserExtensionStoreDesc": { "message": "Help others find out if Bitwarden is right for them. Visit your browser's extension store and leave a rating now." }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Ülemparooli saab muuta Bitwardeni veebirakenduses." }, "fingerprintPhrase": { "message": "Sõrmejälje fraas", @@ -224,7 +224,7 @@ "message": "Logi välja" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "Meist" }, "about": { "message": "Rakenduse info" @@ -233,10 +233,10 @@ "message": "More from Bitwarden" }, "continueToBitwardenDotCom": { - "message": "Continue to bitwarden.com?" + "message": "Mine edasi bitwarden.com-i?" }, "bitwardenForBusiness": { - "message": "Bitwarden for Business" + "message": "Bitwarden Ärikliendile" }, "bitwardenAuthenticator": { "message": "Bitwarden Authenticator" @@ -257,7 +257,7 @@ "message": "Create smooth and secure login experiences free from traditional passwords with Passwordless.dev. Learn more on the bitwarden.com website." }, "freeBitwardenFamilies": { - "message": "Free Bitwarden Families" + "message": "Tasuta Bitwarden Peredele" }, "freeBitwardenFamiliesPageDesc": { "message": "You are eligible for Free Bitwarden Families. Redeem this offer today in the web app." @@ -321,7 +321,7 @@ "message": "Loo oma kontodele tugevaid ja unikaalseid paroole." }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "Bitwardeni veebirakendus" }, "importItems": { "message": "Impordi andmed" @@ -409,13 +409,13 @@ "message": "Lemmik" }, "unfavorite": { - "message": "Unfavorite" + "message": "Eemalda lemmikutest" }, "itemAddedToFavorites": { - "message": "Item added to favorites" + "message": "Ese lisatud lemmikutesse" }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "Ese eemaldatud lemmikutest" }, "notes": { "message": "Märkmed" @@ -439,7 +439,7 @@ "message": "Käivita" }, "launchWebsite": { - "message": "Launch website" + "message": "Ava Veebileht" }, "website": { "message": "Veebileht" @@ -463,10 +463,10 @@ "message": "Set up an unlock method in Settings" }, "sessionTimeoutHeader": { - "message": "Session timeout" + "message": "Sessiooni ajalõpp" }, "otherOptions": { - "message": "Other options" + "message": "Muud valikud" }, "rateExtension": { "message": "Hinda seda laiendust" @@ -509,7 +509,7 @@ "message": "Lukusta paroolihoidla" }, "lockAll": { - "message": "Lock all" + "message": "Lukusta kõik" }, "immediately": { "message": "Koheselt" @@ -556,6 +556,18 @@ "security": { "message": "Turvalisus" }, + "confirmMasterPassword": { + "message": "Kinnita ülemparool" + }, + "masterPassword": { + "message": "Ülemparool" + }, + "masterPassImportant": { + "message": "Ülemparooli ei saa taastada, kui sa selle unustama peaksid!" + }, + "masterPassHintLabel": { + "message": "Vihje ülemparoolile" + }, "errorOccurred": { "message": "Ilmnes viga" }, @@ -588,10 +600,10 @@ "message": "Konto on loodud! Võid nüüd sisse logida." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Sisselogimine õnnestus" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Võid selle akna sulgeda" }, "masterPassSent": { "message": "Ülemparooli vihje saadeti sinu e-postile." @@ -616,7 +628,7 @@ "message": "Automaatne täitmine ebaõnnestus. Palun kopeeri informatsioon käsitsi." }, "totpCaptureError": { - "message": "Unable to scan QR code from the current webpage" + "message": "Ei õnnestunud skännida sellelt lehelt QR-kood" }, "totpCaptureSuccess": { "message": "Authenticator key added" @@ -631,7 +643,7 @@ "message": "Välja logitud" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "Sa logisid oma kontolt välja." }, "loginExpired": { "message": "Sessioon on aegunud." @@ -779,7 +791,7 @@ "message": "Ask to update a login's password when a change is detected on a website. Applies to all logged in accounts." }, "enableUsePasskeys": { - "message": "Ask to save and use passkeys" + "message": "Küsi luba pääsuvõtmete salvestamiseks ja kasutamiseks" }, "usePasskeysDesc": { "message": "Ask to save new passkeys or log in with passkeys stored in your vault. Applies to all logged in accounts." @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Paroolide ajalugu" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Vajalik on e-posti kinnitamine" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Selle funktsiooni kasutamiseks pead kinnitama oma e-posti aadressi. Saad seda teha veebihoidlas." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Pääsuvõti on eemaldatud" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 1c98122849d..414d1764039 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Segurtasuna" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Akats bat gertatu da" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Pasahitz historia" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Egiaztapen emaila beharrezkoa da" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Emaila egiaztatu behar duzu funtzio hau erabiltzeko. Emaila web-eko kutxa gotorrean egiazta dezakezu." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 003b8667fa5..ffc4370f20d 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -556,6 +556,18 @@ "security": { "message": "امنیت" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "خطایی رخ داده است" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "تاریخچه کلمه عبور" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "تأیید ایمیل لازم است" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "برای استفاده از این ویژگی باید ایمیل خود را تأیید کنید. می‌توانید ایمیل خود را در گاوصندوق وب تأیید کنید." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index a8e978b0beb..4758613a2ea 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Suojaus" }, + "confirmMasterPassword": { + "message": "Vahvista pääsalasana" + }, + "masterPassword": { + "message": "Pääsalasana" + }, + "masterPassImportant": { + "message": "Pääsalasanasi palauttaminen ei ole mahdollista, jos unohdat sen!" + }, + "masterPassHintLabel": { + "message": "Pääsalasanan vihje" + }, "errorOccurred": { "message": "Tapahtui virhe" }, @@ -1463,7 +1475,16 @@ } }, "editItemHeader": { - "message": "Muokkaa $TYPE$", + "message": "Muokkaa kohdetta $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, + "viewItemHeader": { + "message": "Näytä $TYPE$", "placeholders": { "type": { "content": "$1", @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Sähköpostiosoite on vahvistettava" }, + "emailVerifiedV2": { + "message": "Sähköpostiosoite on vahvistettu" + }, "emailVerificationRequiredDesc": { "message": "Sinun on vahvistettava sähköpostiosoitteesi käyttääksesi ominaisuutta. Voit vahvistaa osoitteesi verkkoholvissa." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Suojausavain poistettiin" }, - "unassignedItemsBannerNotice": { - "message": "Huomioi: Määrittämättömät organisaatiokohteet eivät enää näy \"Kaikki holvit\" -näkymässä, vaan ne ovat käytettävissä vain Hallintapaneelista." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Huomioi: Alkaen 16. toukokuuta 2024, määrittämättömät organisaatiokohteet eivät enää näy \"Kaikki holvit\" -näkymässä, vaan ne ovat käytettävissä vain Hallintapaneelista." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Määritä nämä kohteet kokoelmaan", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ", jotta ne näkyvät.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Automaattitäytä ehdotukset" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Käytöstä poistettujen organisaatioiden kohteet eivät ole käytettävissä. Ole yhteydessä organisaation omistajaan saadaksesi apua." }, + "additionalInformation": { + "message": "Lisätietoja" + }, + "itemHistory": { + "message": "Kohdehistoria" + }, + "lastEdited": { + "message": "Viimeksi muokattu" + }, + "ownerYou": { + "message": "Omistaja: Sinä" + }, + "linked": { + "message": "Linkitetty" + }, + "copySuccessful": { + "message": "Kopiointi onnistui" + }, "upload": { "message": "Lähetä" }, @@ -3548,7 +3576,7 @@ } }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Haluatko varmasti poistaa tämän liitteen pysyvästi?" + "message": "Haluatko varmasti poistaa liitteen pysyvästi?" }, "premium": { "message": "Premium" @@ -3559,6 +3587,24 @@ "filters": { "message": "Suodattimet" }, + "personalDetails": { + "message": "Henkilökohtaiset tiedot" + }, + "identification": { + "message": "Tunnistautuminen" + }, + "contactInfo": { + "message": "Yhteystiedot" + }, + "downloadAttachment": { + "message": "Lataa - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Kortin tiedot" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 54e7ef33b93..5efa5ca5cf3 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Kaligtasan" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Nagkaroon ng error" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Kasaysayan ng Password" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Kailangan ang pag verify ng email" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Kailangan mong i-verify ang iyong email upang gamitin ang tampok na ito. Maaari mong i-verify ang iyong email sa web vault." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 1278470313a..d0f6aa710cb 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Sécurité" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Une erreur est survenue" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Historique des mots de passe" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Vérification de courriel requise" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Vous devez vérifier votre courriel pour utiliser cette fonctionnalité. Vous pouvez vérifier votre courriel dans le coffre web." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Clé d'identification (passkey) retirée" }, - "unassignedItemsBannerNotice": { - "message": "Remarque : Les éléments d'organisation non assignés ne sont plus visibles dans la vue Tous les coffres et ne sont maintenant accessibles que via la Console Admin." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Remarque : À partir du 16 mai 2024, les éléments d'organisation non assignés ne seront plus visibles dans votre vue Tous les coffres sur les appareils et ne seront maintenant accessibles que via la Console Admin." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Ajouter ces éléments à une collection depuis la", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "pour les rendre visibles.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Suggestions de saisie automatique" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Les éléments des Organisations désactivées ne sont pas accessibles. Contactez le propriétaire de votre Organisation pour obtenir de l'aide." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filtres" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 85239caddb5..c9b15d4ea0a 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Seguridade" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Produciuse un erro" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Historial de contrasinais" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Aviso: O 16 de maio de 2024, os elementos de organización non asignados non serán visíbeis na vista de Todas as caixas fortes e só serán accesíbeis a través da Consola de Administrador." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index ca67598a2e1..31d20be247e 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -556,6 +556,18 @@ "security": { "message": "אבטחה" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "אירעה שגיאה" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "היסטוריית סיסמאות" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 5004beb376b..4b23d203704 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -556,6 +556,18 @@ "security": { "message": "सुरक्षा" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "कोई ग़लती हुई।" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "पासवर्ड इतिहास" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "ईमेल सत्यापन आवश्यक है" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "इस सुविधा का उपयोग करने के लिए आपको अपने ईमेल को सत्यापित करना होगा। आप वेब वॉल्ट में अपने ईमेल को सत्यापित कर सकते हैं।" }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "फ़िल्टर" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 77bc1d37612..266a7b2d323 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Sigurnost" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Došlo je do pogreške" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Povijest" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Potrebna je potvrda e-pošte" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Moraš ovjeriti svoju e-poštu u mrežnom trezoru za koritšenje ove značajke." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index d4106a0e555..598f122b4f8 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Biztonság" }, + "confirmMasterPassword": { + "message": "Mesterjelszó megerősítése" + }, + "masterPassword": { + "message": "Mesterjelszó" + }, + "masterPassImportant": { + "message": "A mesterjelszó nem állítható helyre, ha elfelejtik!" + }, + "masterPassHintLabel": { + "message": "Mesterjelszó emlékeztető" + }, "errorOccurred": { "message": "Hiba történt." }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "$TYPE$ megtekintése", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Jelszó előzmények" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email hitelesítés szükséges" }, + "emailVerifiedV2": { + "message": "Az email cím ellenőrzésre került." + }, "emailVerificationRequiredDesc": { "message": "A funkció használatához igazolni kell email címet. Az email cím a webtárban ellenőrizhető." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "A jelszó eltávolításra került." }, - "unassignedItemsBannerNotice": { - "message": "Megjegyzés: A nem hozzárendelt szervezeti elemek már nem láthatók az Összes széf nézetben a különböző eszközökön és csak az Adminisztrátori konzolon keresztül érhetők el." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Megjegyzés: 2024. május 16-tól a nem hozzárendelt szervezeti elemek többé nem lesznek láthatók az Összes széf nézetben a különböző eszközökön és csak az Adminisztrátori konzolon keresztül lesznek elérhetők." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Rendeljük hozzá ezeket az elemeket a gyűjteményhez", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "a láthatósághoz.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Automatikus kitöltés javaslatok" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "További információ" + }, + "itemHistory": { + "message": "Elem előzmény" + }, + "lastEdited": { + "message": "Utoljára szerkesztve" + }, + "ownerYou": { + "message": "Tulajdonos: Én" + }, + "linked": { + "message": "Csatolva" + }, + "copySuccessful": { + "message": "A másolás sikeres volt." + }, "upload": { "message": "Feltöltés" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Szűrők" }, + "personalDetails": { + "message": "Személyes adatok" + }, + "identification": { + "message": "Azonosítás" + }, + "contactInfo": { + "message": "Kapcsolat infó" + }, + "downloadAttachment": { + "message": "Letöltés - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Kártyaadatok" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 4e92f007942..8625ce67000 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Keamanan" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Terjadi kesalahan" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Riwayat Kata Sandi" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Verifikasi Email Diperlukan" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Anda harus memverifikasi email Anda untuk menggunakan fitur ini. Anda dapat memverifikasi email Anda di brankas web." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 219728ced0a..b4f3e1d240d 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Sicurezza" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Si è verificato un errore" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Cronologia delle password" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Verifica email obbligatoria" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Devi verificare la tua email per usare questa funzionalità. Puoi verificare la tua email nella cassaforte web." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey rimossa" }, - "unassignedItemsBannerNotice": { - "message": "Avviso: gli elementi dell'organizzazione non assegnati non sono più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e sono ora accessibili solo tramite la Console di amministrazione." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Avviso: dal 16 maggio 2024, gli elementi dell'organizzazione non assegnati non saranno più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e saranno accessibili solo tramite la Console di amministrazione." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assegna questi elementi ad una raccolta dalla", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "per renderli visibili.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Suggerimenti per il riempimento automatico" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Non puoi accedere agli elementi nelle organizzazioni disattivate. Contatta il proprietario della tua organizzazione per ricevere assistenza." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index f8ead624a11..ad45122d57b 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -556,6 +556,18 @@ "security": { "message": "セキュリティ" }, + "confirmMasterPassword": { + "message": "マスターパスワードの確認" + }, + "masterPassword": { + "message": "マスターパスワード" + }, + "masterPassImportant": { + "message": "マスターパスワードを忘れた場合は復元できません!" + }, + "masterPassHintLabel": { + "message": "マスターパスワードのヒント" + }, "errorOccurred": { "message": "エラーが発生しました" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "$TYPE$ を表示", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "パスワードの履歴" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "メールアドレスの確認が必要です" }, + "emailVerifiedV2": { + "message": "メールアドレスを認証しました" + }, "emailVerificationRequiredDesc": { "message": "この機能を使用するにはメールアドレスを確認する必要があります。ウェブ保管庫でメールアドレスを確認できます。" }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "パスキーを削除しました" }, - "unassignedItemsBannerNotice": { - "message": "注意: 割り当てられていない組織アイテムは、すべての保管庫ビューでは表示されなくなり、管理コンソールからのみアクセスできるようになります。" - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "お知らせ:2024年5月16日に、 割り当てられていない組織アイテムは、すべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。" - }, - "unassignedItemsBannerCTAPartOne": { - "message": "これらのアイテムのコレクションへの割り当てを", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "で実行すると表示できるようになります。", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "候補を自動入力する" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "無効化された組織のアイテムにアクセスすることはできません。組織の所有者に連絡してください。" }, + "additionalInformation": { + "message": "その他の情報" + }, + "itemHistory": { + "message": "アイテム履歴" + }, + "lastEdited": { + "message": "最終更新日" + }, + "ownerYou": { + "message": "所有者: あなた" + }, + "linked": { + "message": "リンク済" + }, + "copySuccessful": { + "message": "コピーしました" + }, "upload": { "message": "アップロード" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "フィルター" }, + "personalDetails": { + "message": "個人情報" + }, + "identification": { + "message": "ID" + }, + "contactInfo": { + "message": "連絡先情報" + }, + "downloadAttachment": { + "message": "ダウンロード - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "カード情報" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 4c2ca642151..df8dc0cce3e 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -556,6 +556,18 @@ "security": { "message": "უსაფრთხოება" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "დაფიქსირდა შეცდომა" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index d1412681ac2..fa76cf7060a 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index ebc97ca1220..fc2b2711c9f 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -556,6 +556,18 @@ "security": { "message": "ಭದ್ರತೆ" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "ದೋಷ ಸಂಭವಿಸಿದೆ" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "ಪಾಸ್ವರ್ಡ್ ಇತಿಹಾಸ" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "ಇಮೇಲ್ ಪರಿಶೀಲನೆ ಅಗತ್ಯವಿದೆ" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "ಈ ವೈಶಿಷ್ಟ್ಯವನ್ನು ಬಳಸಲು ನಿಮ್ಮ ಇಮೇಲ್ ಅನ್ನು ನೀವು ಪರಿಶೀಲಿಸಬೇಕು. ವೆಬ್ ವಾಲ್ಟ್ನಲ್ಲಿ ನಿಮ್ಮ ಇಮೇಲ್ ಅನ್ನು ನೀವು ಪರಿಶೀಲಿಸಬಹುದು." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 5b33655c21a..9762761b366 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -556,6 +556,18 @@ "security": { "message": "보안" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "오류가 발생했습니다" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "비밀번호 변경 기록" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "이메일 인증 필요함" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "이 기능을 사용하려면 이메일 인증이 필요합니다. 웹 보관함에서 이메일을 인증할 수 있습니다." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "패스키 제거됨" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index e773c9a83b0..f09e9c21caa 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Apsauga" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Įvyko klaida" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Slaptažodžio istorija" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Reikalingas elektroninio pašto patvirtinimas" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Turite patvirtinti savo el. paštą, kad galėtumėte naudotis šia funkcija. Savo el. pašto adresą galite patvirtinti žiniatinklio saugykloje." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Pašalintas slaptaraktis" }, - "unassignedItemsBannerNotice": { - "message": "Pranešimas: nepriskirti organizacijos elementai nebėra matomi peržiūros rodinyje Visi saugyklos ir yra pasiekiami tik per Administratoriaus konsolę." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Pranešimas: 2024 m. gegužės 16 d. nepriskirti organizacijos elementai nebėra matomi peržiūros rodinyje Visi saugyklos ir yra pasiekiami tik per Administratoriaus konsolę." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Priskirkite šiuos elementus kolekcijai iš", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ", kad jie būtų matomi.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3493,13 +3503,13 @@ "message": "Items with no folder" }, "itemDetails": { - "message": "Item details" + "message": "Elemento informacija" }, "itemName": { - "message": "Item name" + "message": "Elemento pavadinimas" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Negalite pašalinti kolekcijų su Peržiūrėti tik leidimus: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3521,33 @@ "message": "Organization is deactivated" }, "owner": { - "message": "Owner" + "message": "Savininkas" }, "selfOwnershipLabel": { - "message": "You", + "message": "Jūs", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Įkelti" }, @@ -3559,16 +3587,37 @@ "filters": { "message": "Filtrai" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { - "message": "Card details" + "message": "Kortelės duomenys" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "„$BRAND$“ duomenys", "placeholders": { "brand": { "content": "$1", "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 633f8069826..02688c4943f 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Drošība" }, + "confirmMasterPassword": { + "message": "Apstiprināt galveno paroli" + }, + "masterPassword": { + "message": "Galvenā parole" + }, + "masterPassImportant": { + "message": "Galveno paroli nevar atgūt, ja tā tiek aizmirsta." + }, + "masterPassHintLabel": { + "message": "Galvenās paroles norāde" + }, "errorOccurred": { "message": "Atgadījās kļūda" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Apskatīt $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Paroļu vēsture" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Nepieciešama e-pasta adreses apstiprināšana" }, + "emailVerifiedV2": { + "message": "E-pasta adrese ir apliecināta" + }, "emailVerificationRequiredDesc": { "message": "Ir nepieciešams apstiprināt e-pasta adresi, lai būtu iespējams izmantot šo iespēju. To var izdarīt tīmekļa glabātavā." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Piekļuves atslēga noņemta" }, - "unassignedItemsBannerNotice": { - "message": "Jāņem vērā: nepiešķirti apvienības vienumi vairs nav redzami skatā \"Visas glabātavas\" un ir pieejami tikai pārvaldības konsolē." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Jāņem vērā: no 2024. gada 16. maija nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" un būs pieejami tikai pārvaldības konsolē." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Piešķirt šos vienumus krājumam", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "lai padarītu tos redzamus.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Ieteikumi automātiskajai aizpildei" }, @@ -3493,13 +3503,13 @@ "message": "Vienumi bez mapes" }, "itemDetails": { - "message": "Item details" + "message": "Vienuma dati" }, "itemName": { - "message": "Item name" + "message": "Vienuma nosaukums" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Nevar noņemt krājumus ar tiesībām \"Tikai skatīt\": $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3521,33 @@ "message": "Apvienība ir atspējota" }, "owner": { - "message": "Owner" + "message": "Īpašnieks" }, "selfOwnershipLabel": { - "message": "You", + "message": "Tu", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Atspējotu apvienību vienumiem nevar piekļūt. Jāsazinās ar apvienības īpašnieku, lai iegūtu palīdzību." }, + "additionalInformation": { + "message": "Papildu informācija" + }, + "itemHistory": { + "message": "Vienuma vēsture" + }, + "lastEdited": { + "message": "Pēdējo reizi labots" + }, + "ownerYou": { + "message": "Īpašnieks: Tu" + }, + "linked": { + "message": "Saistīts" + }, + "copySuccessful": { + "message": "Ievietošana starpliktuvē veiksmīga" + }, "upload": { "message": "Augšupielādēt" }, @@ -3559,16 +3587,37 @@ "filters": { "message": "Atlases" }, + "personalDetails": { + "message": "Personiskā informācija" + }, + "identification": { + "message": "Identifikācija" + }, + "contactInfo": { + "message": "Saziņas informācija" + }, + "downloadAttachment": { + "message": "Lejupielādēt $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { - "message": "Card details" + "message": "Kartes dati" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "$BRAND$ dati", "placeholders": { "brand": { "content": "$1", "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 81c3608d10a..7d53653e2c8 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -556,6 +556,18 @@ "security": { "message": "സുരക്ഷ" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "ഒരു പിഴവ് സംഭവിച്ചിരിക്കുന്നു" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "പാസ്സ്‌വേഡ് നാൾവഴി" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 73b928ebe0c..3cf67b119b2 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index d1412681ac2..fa76cf7060a 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 3c07e43ff96..33c362d5b87 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Sikkerhet" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "En feil har oppstått" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Passordhistorikk" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "E-postbekreftelse kreves" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Du må bekrefte e-posten din for å bruke denne funksjonen. Du kan bekrefte e-postadressen din i netthvelvet." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index d1412681ac2..fa76cf7060a 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 98cd9ca449d..b3d04e67c75 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Beveiliging" }, + "confirmMasterPassword": { + "message": "Hoofdwachtwoord bevestigen" + }, + "masterPassword": { + "message": "Hoofdwachtwoord" + }, + "masterPassImportant": { + "message": "Je kunt je hoofdwachtwoord niet herstellen als je het vergeet!" + }, + "masterPassHintLabel": { + "message": "Hoofdwachtwoordhint" + }, "errorOccurred": { "message": "Er is een fout opgetreden" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "$TYPE$ weergeven", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Geschiedenis" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "E-mailverificatie vereist" }, + "emailVerifiedV2": { + "message": "E-mailadres geverifieerd" + }, "emailVerificationRequiredDesc": { "message": "Je moet je e-mailadres verifiëren om deze functie te gebruiken. Je kunt je e-mailadres verifiëren in de kluis." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey verwijderd" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Suggesties voor automatisch invullen" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in een gedeactiveerde organisatie zijn niet toegankelijk. Neem contact op met de eigenaar van je organisatie voor hulp." }, + "additionalInformation": { + "message": "Aanvullende informatie" + }, + "itemHistory": { + "message": "Itemgeschiedenis" + }, + "lastEdited": { + "message": "Laatst gewijzigd" + }, + "ownerYou": { + "message": "Eigenaar: Jij" + }, + "linked": { + "message": "Gekoppeld" + }, + "copySuccessful": { + "message": "Kopiëren gelukt" + }, "upload": { "message": "Uploaden" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Persoonlijke gegevens" + }, + "identification": { + "message": "Identificatie" + }, + "contactInfo": { + "message": "Contactgegevens" + }, + "downloadAttachment": { + "message": "$ITEMNAME$ downloaden", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Kaartgegevens" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index d1412681ac2..fa76cf7060a 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index d1412681ac2..fa76cf7060a 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 40ba6659fb0..1603314a616 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Zabezpieczenia" }, + "confirmMasterPassword": { + "message": "Potwierdź hasło główne" + }, + "masterPassword": { + "message": "Hasło główne" + }, + "masterPassImportant": { + "message": "Twoje hasło główne nie może zostać odzyskane, jeśli je zapomnisz!" + }, + "masterPassHintLabel": { + "message": "Podpowiedź do hasła głównego" + }, "errorOccurred": { "message": "Wystąpił błąd" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Zobacz $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Historia hasła" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Weryfikacja adresu e-mail jest wymagana" }, + "emailVerifiedV2": { + "message": "E-mail zweryfikowany" + }, "emailVerificationRequiredDesc": { "message": "Musisz zweryfikować adres e-mail, aby korzystać z tej funkcji. Adres możesz zweryfikować w sejfie internetowym." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey został usunięty" }, - "unassignedItemsBannerNotice": { - "message": "Uwaga: Nieprzypisane elementy organizacji nie są już widoczne w widoku Wszystkie sejfy i są teraz dostępne tylko przez Konsolę Administracyjną." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Uwaga: 16 maja 2024 r. nieprzypisana elementy w organizacji nie będą już widoczne w widoku Wszystkie sejfy i będą dostępne tylko przez Konsolę Administracyjną." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Przypisz te elementy do kolekcji z", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ", aby były widoczne.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Sugestie autouzupełnienia" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Nie można uzyskać dostępu do elementów w wyłączonych organizacjach. Skontaktuj się z właścicielem organizacji, aby uzyskać pomoc." }, + "additionalInformation": { + "message": "Dodatkowe informacje" + }, + "itemHistory": { + "message": "Historia elementu" + }, + "lastEdited": { + "message": "Ostatnio edytowany" + }, + "ownerYou": { + "message": "Właściciel: Ty" + }, + "linked": { + "message": "Powiązane" + }, + "copySuccessful": { + "message": "Kopiowanie zakończone sukcesem" + }, "upload": { "message": "Wyślij" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filtry" }, + "personalDetails": { + "message": "Dane osobowe" + }, + "identification": { + "message": "Tożsamość" + }, + "contactInfo": { + "message": "Daje kontaktowe" + }, + "downloadAttachment": { + "message": "Pobierz - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Szczegóły karty" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 76dd0025851..a9161bde22c 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -50,7 +50,7 @@ "message": "Uma dica de senha mestra pode ajudá-lo(a) a lembrá-lo(a) caso você esqueça." }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Se você esquecer sua senha, a dica de senha pode ser enviada ao seu e-mail. $CURRENT$/$MAXIMUM$ caracteres máximos.", "placeholders": { "current": { "content": "$1", @@ -186,7 +186,7 @@ "message": "Confirme a sua identidade para continuar." }, "changeMasterPassword": { - "message": "Alterar Senha Mestra" + "message": "Alterar senha mestra" }, "continueToWebApp": { "message": "Continuar no aplicativo web?" @@ -556,6 +556,18 @@ "security": { "message": "Segurança" }, + "confirmMasterPassword": { + "message": "Confirme a senha mestra" + }, + "masterPassword": { + "message": "Senha mestra" + }, + "masterPassImportant": { + "message": "Sua senha mestra não pode ser recuperada se você a esquecer!" + }, + "masterPassHintLabel": { + "message": "Dica da senha mestra" + }, "errorOccurred": { "message": "Ocorreu um erro" }, @@ -855,7 +867,7 @@ "message": "Esta senha será usada para exportar e importar este arquivo" }, "accountRestrictedOptionDescription": { - "message": "Use sua chave criptográfica da conta, derivada do nome de usuário e Senha Mestra da sua conta, para criptografar a exportação e restringir importação para apenas a conta atual do Bitwarden." + "message": "Use sua chave criptográfica da conta, derivada do nome de usuário e senha mestra da sua conta, para criptografar a exportação e restringir importação para apenas a conta atual do Bitwarden." }, "passwordProtectedOptionDescription": { "message": "Defina uma senha de arquivo para criptografar a exportação e importá-la para qualquer conta do Bitwarden usando a senha para descriptografia." @@ -1106,17 +1118,17 @@ "message": "Aplicativo de Autenticação" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Insira um código gerado por um aplicativo autenticador como o Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP Security Key" + "message": "Chave de Segurança Yubico OTP" }, "yubiKeyDesc": { "message": "Utilize uma YubiKey para acessar a sua conta. Funciona com YubiKey 4, 4 Nano, 4C, e dispositivos NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Insira um código gerado pelo Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1133,7 +1145,7 @@ "message": "E-mail" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Digite o código enviado para seu e-mail." }, "selfHostedEnvironment": { "message": "Ambiente Auto-hospedado" @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Visualizar $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Histórico de Senha" }, @@ -1629,7 +1650,7 @@ "description": "ex. A weak password. Scale: Weak -> Good -> Strong" }, "weakMasterPassword": { - "message": "Senha Mestra Fraca" + "message": "Senha mestra fraca" }, "weakMasterPasswordDesc": { "message": "A senha mestra que você selecionou está fraca. Você deve usar uma senha mestra forte (ou uma frase-passe) para proteger a sua conta Bitwarden adequadamente. Tem certeza que deseja usar esta senha mestra?" @@ -1746,7 +1767,7 @@ } }, "setMasterPassword": { - "message": "Definir Senha Mestra" + "message": "Definir senha mestra" }, "currentMasterPass": { "message": "Senha mestra atual" @@ -2164,17 +2185,20 @@ "emailVerificationRequired": { "message": "Verificação de E-mail Necessária" }, + "emailVerifiedV2": { + "message": "E-mail verificado" + }, "emailVerificationRequiredDesc": { "message": "Você precisa verificar o seu e-mail para usar este recurso. Você pode verificar seu e-mail no cofre web." }, "updatedMasterPassword": { - "message": "Senha Mestra Atualizada" + "message": "Senha mestra atualizada" }, "updateMasterPassword": { - "message": "Atualizar Senha Mestra" + "message": "Atualizar senha mestra" }, "updateMasterPasswordWarning": { - "message": "Sua Senha Mestra foi alterada recentemente por um administrador de sua organização. Para acessar o cofre, você precisa atualizá-la agora. O processo desconectará você da sessão atual, exigindo que você inicie a sessão novamente. Sessões ativas em outros dispositivos podem continuar ativas por até uma hora." + "message": "Sua senha mestra foi alterada recentemente por um administrador de sua organização. Para acessar o cofre, você precisa atualizá-la agora. O processo desconectará você da sessão atual, exigindo que você inicie a sessão novamente. Sessões ativas em outros dispositivos podem continuar ativas por até uma hora." }, "updateWeakMasterPasswordWarning": { "message": "A sua senha mestra não atende a uma ou mais das políticas da sua organização. Para acessar o cofre, você deve atualizar a sua senha mestra agora. O processo desconectará você da sessão atual, exigindo que você inicie a sessão novamente. Sessões ativas em outros dispositivos podem continuar ativas por até uma hora." @@ -2277,7 +2301,7 @@ "message": "Sair da Organização" }, "removeMasterPassword": { - "message": "Remover Senha Mestra" + "message": "Remover senha mestra" }, "removedMasterPassword": { "message": "Senha mestra removida." @@ -2576,13 +2600,13 @@ "message": "Login iniciado" }, "exposedMasterPassword": { - "message": "Senha Mestra comprometida" + "message": "Senha mestra comprometida" }, "exposedMasterPasswordDesc": { "message": "A senha foi encontrada em um vazamento de dados. Use uma senha única para proteger sua conta. Tem certeza de que deseja usar uma senha já exposta?" }, "weakAndExposedMasterPassword": { - "message": "Senha Mestra fraca e comprometida" + "message": "Senha mestra fraca e comprometida" }, "weakAndBreachedMasterPasswordDesc": { "message": "Senha fraca identificada e encontrada em um vazamento de dados. Use uma senha forte e única para proteger a sua conta. Tem certeza de que deseja usar essa senha?" @@ -2594,7 +2618,7 @@ "message": "Importante:" }, "masterPasswordHint": { - "message": "Sua Senha Mestra não pode ser recuperada se você a esquecer!" + "message": "Sua senha mestra não pode ser recuperada se você a esquecer!" }, "characterMinimum": { "message": "$LENGTH$ caracteres mínimos", @@ -2886,11 +2910,11 @@ "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { - "message": "Desative o prompt de senha mestra para editar este campo", + "message": "Desative a re-solicitação de senha mestra para editar este campo", "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "Ativar/desativar navegação lateral" }, "skipToContent": { "message": "Ir para o conteúdo" @@ -3108,7 +3132,7 @@ "message": "Confirmar senha do arquivo" }, "exportSuccess": { - "message": "Vault data exported" + "message": "Dados do cofre exportados" }, "typePasskey": { "message": "Chave de acesso" @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Chave de acesso removida" }, - "unassignedItemsBannerNotice": { - "message": "Aviso: Itens da organização não atribuídos não estão mais visíveis na visualização Todos os Cofres e só são acessíveis por meio do painel de administração." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Aviso: Em 16 de maio, 2024, itens da organização não serão mais visíveis na visualização Todos os Cofres e só serão acessíveis por meio do painel de administração." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Atribua estes itens a uma coleção da", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "para torná-los visíveis.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Sugestões de autopreenchimento" }, @@ -3493,10 +3503,10 @@ "message": "Itens sem pasta" }, "itemDetails": { - "message": "Item details" + "message": "Detalhes dos item" }, "itemName": { - "message": "Item name" + "message": "Nome do item" }, "cannotRemoveViewOnlyCollections": { "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", @@ -3514,14 +3524,32 @@ "message": "Owner" }, "selfOwnershipLabel": { - "message": "You", + "message": "Você", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Itens em organizações desativadas não podem ser acessados. Entre em contato com o proprietário da sua organização para obter assistência." }, + "additionalInformation": { + "message": "Informação adicional" + }, + "itemHistory": { + "message": "Histórico do item" + }, + "lastEdited": { + "message": "Última edição" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { - "message": "Upload" + "message": "Fazer upload" }, "addAttachment": { "message": "Add attachment" @@ -3554,10 +3582,28 @@ "message": "Premium" }, "freeOrgsCannotUseAttachments": { - "message": "Free organizations cannot use attachments" + "message": "Organizações gratuitas não podem usar anexos" }, "filters": { - "message": "Filters" + "message": "Filtros" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } }, "cardDetails": { "message": "Card details" @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 5578ac37a6e..dc64dfa8a7d 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -556,17 +556,29 @@ "security": { "message": "Segurança" }, + "confirmMasterPassword": { + "message": "Confirmar a palavra-passe mestra" + }, + "masterPassword": { + "message": "Palavra-passe mestra" + }, + "masterPassImportant": { + "message": "A sua palavra-passe mestra não pode ser recuperada se a esquecer!" + }, + "masterPassHintLabel": { + "message": "Dica da palavra-passe mestra" + }, "errorOccurred": { "message": "Ocorreu um erro" }, "emailRequired": { - "message": "É necessário o endereço de e-mail." + "message": "O endereço de e-mail é obrigatório." }, "invalidEmail": { "message": "Endereço de e-mail inválido." }, "masterPasswordRequired": { - "message": "É necessária a palavra-passe mestra." + "message": "A palavra-passe mestra é obrigatória." }, "confirmMasterPasswordRequired": { "message": "É necessário reescrever a palavra-passe mestra." @@ -649,7 +661,7 @@ "message": "Ocorreu um erro inesperado." }, "nameRequired": { - "message": "É necessário o nome." + "message": "O nome é obrigatório." }, "addedFolder": { "message": "Pasta adicionada" @@ -1082,7 +1094,7 @@ "message": "Abrir novo separador" }, "webAuthnAuthenticate": { - "message": "Autenticar WebAuthn" + "message": "Autenticar o WebAuthn" }, "loginUnavailable": { "message": "Início de sessão indisponível" @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Ver $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Histórico de palavras-passe" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Verificação de e-mail necessária" }, + "emailVerifiedV2": { + "message": "E-mail verificado" + }, "emailVerificationRequiredDesc": { "message": "Tem de verificar o seu e-mail para utilizar esta funcionalidade. Pode verificar o seu e-mail no cofre Web." }, @@ -2274,7 +2298,7 @@ } }, "leaveOrganization": { - "message": "Deixar a organização" + "message": "Sair da organização" }, "removeMasterPassword": { "message": "Remover palavra-passe mestra" @@ -2283,7 +2307,7 @@ "message": "Palavra-passe mestra removida" }, "leaveOrganizationConfirmation": { - "message": "Tem a certeza de que pretende deixar esta organização?" + "message": "Tem a certeza de que pretende sair desta organização?" }, "leftOrganization": { "message": "Saiu da organização." @@ -2739,10 +2763,10 @@ "message": "Dispositivo de confiança" }, "inputRequired": { - "message": "Campo necessário." + "message": "Campo obrigatório." }, "required": { - "message": "necessário" + "message": "obrigatório" }, "search": { "message": "Procurar" @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Chave de acesso removida" }, - "unassignedItemsBannerNotice": { - "message": "Aviso: Os itens da organização não atribuídos já não são visíveis na vista Todos os cofres e só são acessíveis através da Consola de administração." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Aviso: A 16 de maio de 2024, os itens da organização não atribuídos deixarão de estar visíveis na vista Todos os cofres e só estarão acessíveis através da consola de administração." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Atribua estes itens a uma coleção a partir da", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "para os tornar visíveis.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Sugestões de preenchimento automático" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Não é possível aceder aos itens de organizações desativadas. Contacte o proprietário da organização para obter assistência." }, + "additionalInformation": { + "message": "Informações adicionais" + }, + "itemHistory": { + "message": "Histórico do item" + }, + "lastEdited": { + "message": "Última edição" + }, + "ownerYou": { + "message": "Proprietário: Eu" + }, + "linked": { + "message": "Associado" + }, + "copySuccessful": { + "message": "Cópia bem-sucedida" + }, "upload": { "message": "Carregar" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filtros" }, + "personalDetails": { + "message": "Dados pessoais" + }, + "identification": { + "message": "Identificação" + }, + "contactInfo": { + "message": "Informações de contacto" + }, + "downloadAttachment": { + "message": "Transferir - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Detalhes do cartão" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 02e92106c44..3b8809f22dc 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Securitate" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "S-a produs o eroare" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Istoric parole" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Verificare e-mail necesară" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Trebuie să vă verificați e-mailul pentru a utiliza această caracteristică. Puteți verifica e-mailul în seiful web." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index bb9a030a6fa..e5b303c7ea6 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Безопасность" }, + "confirmMasterPassword": { + "message": "Подтвердите мастер-пароль" + }, + "masterPassword": { + "message": "Мастер-пароль" + }, + "masterPassImportant": { + "message": "Ваш мастер-пароль невозможно восстановить, если вы его забудете!" + }, + "masterPassHintLabel": { + "message": "Подсказка к мастер-паролю" + }, "errorOccurred": { "message": "Произошла ошибка" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Просмотр $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "История паролей" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Требуется подтверждение электронной почты" }, + "emailVerifiedV2": { + "message": "Email подтвержден" + }, "emailVerificationRequiredDesc": { "message": "Для использования этой функции необходимо подтвердить ваш email. Вы можете это сделать в веб-хранилище." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey удален" }, - "unassignedItemsBannerNotice": { - "message": "Уведомление: Неприсвоенные элементы организации больше не видны в представлении \"Все хранилища\" и доступны только через консоль администратора." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Уведомление: с 16 мая 2024 года не назначенные элементы организации больше не будут видны в представлении \"Все хранилища\" и будут доступны только через консоль администратора." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Назначьте эти элементы в коллекцию из", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "чтобы сделать их видимыми.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Предложения по автозаполнению" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Доступ к элементам в деактивированных организациях невозможен. Обратитесь за помощью к владельцу организации." }, + "additionalInformation": { + "message": "Дополнительная информация" + }, + "itemHistory": { + "message": "История элемента" + }, + "lastEdited": { + "message": "Последнее изменение" + }, + "ownerYou": { + "message": "Владелец: вы" + }, + "linked": { + "message": "Связано" + }, + "copySuccessful": { + "message": "Скопировано успешно" + }, "upload": { "message": "Загрузить" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Фильтры" }, + "personalDetails": { + "message": "Личные данные" + }, + "identification": { + "message": "Идентификация" + }, + "contactInfo": { + "message": "Контактная информация" + }, + "downloadAttachment": { + "message": "Скачать - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Реквизиты карты" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 0da68c198cd..1416bea9582 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -556,6 +556,18 @@ "security": { "message": "ආරක්ෂාව" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "දෝෂයක් සිදුවී ඇත" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "මුරපද ඉතිහාසය" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "ඊමේල් සත්යාපනය අවශ්ය වේ" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "මෙම අංගය භාවිතා කිරීම සඳහා ඔබේ විද්යුත් තැපෑල සත්යාපනය කළ යුතුය. වෙබ් සුරක්ෂිතාගාරයේ ඔබගේ විද්යුත් තැපෑල සත්යාපනය කළ හැකිය." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index d190bb1925d..c375af5ffa9 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Zabezpečenie" }, + "confirmMasterPassword": { + "message": "Potvrdiť hlavné heslo" + }, + "masterPassword": { + "message": "Hlavné heslo" + }, + "masterPassImportant": { + "message": "Vaše hlavné heslo sa nebude dať obnoviť, ak ho zabudnete!" + }, + "masterPassHintLabel": { + "message": "Nápoveda pre hlavné heslo" + }, "errorOccurred": { "message": "Vyskytla sa chyba" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Zobraziť $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "História hesla" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Vyžaduje sa overenie e-mailu" }, + "emailVerifiedV2": { + "message": "Overený e-mail" + }, "emailVerificationRequiredDesc": { "message": "Na použitie tejto funkcie musíte overiť svoj e-mail. Svoj e-mail môžete overiť vo webovom trezore." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Prístupový kľúč bol odstránený" }, - "unassignedItemsBannerNotice": { - "message": "Upozornenie: Nepriradené položky organizácie už nie sú viditeľné v zobrazení Všetky trezory a sú prístupné iba cez Správcovskú konzolu." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Upozornenie: 16. mája 2024 nepriradené položky organizácie už nebudú viditeľné v zobrazení Všetky trezory a budú prístupné iba cez Správcovskú konzolu." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Priradiť tieto položky do zbierky zo", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ", aby boli viditeľné.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Návrhy automatického vypĺňania" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "K položkám vo vypnutej organizácii nie je možné pristupovať. Požiadajte o pomoc vlastníka organizácie." }, + "additionalInformation": { + "message": "Ďalšie informácie" + }, + "itemHistory": { + "message": "História položky" + }, + "lastEdited": { + "message": "Posledná úprava" + }, + "ownerYou": { + "message": "Vlastník: Vy" + }, + "linked": { + "message": "Prepojené" + }, + "copySuccessful": { + "message": "Úspešne skopírované" + }, "upload": { "message": "Nahrať" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filtre" }, + "personalDetails": { + "message": "Osobné údaje" + }, + "identification": { + "message": "Identifikácia" + }, + "contactInfo": { + "message": "Kontaktné informácie" + }, + "downloadAttachment": { + "message": "Stiahnuť – $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Podrobnosti o karte" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 39ab221327b..e53e830b006 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Varnost" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Prišlo je do napake" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Zgodovina gesel" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Potrebna je potrditev e-naslova" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Za uporabo te funkcionalnosti morate potrditi svoj e-naslov. To lahko storite v spletnem trezorju." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index ac20e402f10..02a3f824521 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Сигурност" }, + "confirmMasterPassword": { + "message": "Потрдити главну лозинку" + }, + "masterPassword": { + "message": "Главна Лозинка" + }, + "masterPassImportant": { + "message": "Ваша главна лозинка се не може повратити ако је заборавите!" + }, + "masterPassHintLabel": { + "message": "Савет главне лозинке" + }, "errorOccurred": { "message": "Дошло је до грешке!" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Видети $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Историја Лозинке" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Потребна је верификација е-поште" }, + "emailVerifiedV2": { + "message": "Имејл верификован" + }, "emailVerificationRequiredDesc": { "message": "Морате да потврдите е-пошту да бисте користили ову функцију. Можете да потврдите е-пошту у веб сефу." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Приступачни кључ је уклоњен" }, - "unassignedItemsBannerNotice": { - "message": "Напомена: Недодељене ставке организације више нису видљиве у приказу Сви сефови и доступне су само преко Админ конзоле." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Напомена: од 16 Маја 2024м недодељене ставке организације више нису видљиве у приказу Сви сефови и доступне су само преко Админ конзоле." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Предлози за ауто-попуњавање" }, @@ -3493,13 +3503,13 @@ "message": "Ставке без фасцикле" }, "itemDetails": { - "message": "Item details" + "message": "Детаљи ставке" }, "itemName": { - "message": "Item name" + "message": "Име ставке" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Не можете уклонити колекције са дозволама само за приказ: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3521,33 @@ "message": "Организација је деактивирана" }, "owner": { - "message": "Owner" + "message": "Власник" }, "selfOwnershipLabel": { - "message": "You", + "message": "Ти", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Није могуће приступити ставкама у деактивираним организацијама. Обратите се власнику ваше организације за помоћ." }, + "additionalInformation": { + "message": "Додатне информације" + }, + "itemHistory": { + "message": "Историја предмета" + }, + "lastEdited": { + "message": "Последња измена" + }, + "ownerYou": { + "message": "Власник: Ви" + }, + "linked": { + "message": "Повезано" + }, + "copySuccessful": { + "message": "Копија успешна" + }, "upload": { "message": "Отпреми" }, @@ -3559,16 +3587,37 @@ "filters": { "message": "Филтери" }, + "personalDetails": { + "message": "Личне информације" + }, + "identification": { + "message": "Идентификација" + }, + "contactInfo": { + "message": "Контакт инфо" + }, + "downloadAttachment": { + "message": "Преузми - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { - "message": "Card details" + "message": "Детаљи картице" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "$BRAND$ детаљи", "placeholders": { "brand": { "content": "$1", "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index dc351bc6878..3444a42c364 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Säkerhet" }, + "confirmMasterPassword": { + "message": "Bekräfta huvudlösenord" + }, + "masterPassword": { + "message": "Huvudlösenord" + }, + "masterPassImportant": { + "message": "Ditt huvudlösenord kan inte återställas om du glömmer det!" + }, + "masterPassHintLabel": { + "message": "Huvudlösenordsledtråd" + }, "errorOccurred": { "message": "Ett fel har uppstått" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Visa $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Lösenordshistorik" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "E-postverifiering krävs" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Du måste verifiera din e-postadress för att använda den här funktionen. Du kan verifiera din e-postadress i webbvalvet." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey borttagen" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "för att göra dem synliga.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Ytterligare information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Ägare: Du" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Ladda upp" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index d1412681ac2..fa76cf7060a 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index a1e8479da87..8cd62631f36 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -556,6 +556,18 @@ "security": { "message": "ความปลอดภัย" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "พบข้อผิดพลาด" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "ประวัติของรหัสผ่าน" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 20114a5fd8e..4af23e28a36 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Güvenlik" }, + "confirmMasterPassword": { + "message": "Ana parolayı onaylayın" + }, + "masterPassword": { + "message": "Ana parola" + }, + "masterPassImportant": { + "message": "Ana parolanızı unutursanız kurtaramazsınız!" + }, + "masterPassHintLabel": { + "message": "Ana parola ipucu" + }, "errorOccurred": { "message": "Bir hata oluştu" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Parola geçmişi" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "E-posta doğrulaması gerekiyor" }, + "emailVerifiedV2": { + "message": "E-posta doğrulandı" + }, "emailVerificationRequiredDesc": { "message": "Bu özelliği kullanmak için e-postanızı doğrulamanız gerekir. E-postanızı web kasasında doğrulayabilirsiniz." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Geçiş anahtarı kaldırıldı" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Önerileri otomatik doldur" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Pasif kuruluşlardaki kayıtlara erişilemez. Destek almak için kuruluş sahibinizle iletişime geçin." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filtreler" }, + "personalDetails": { + "message": "Kişisel bilgiler" + }, + "identification": { + "message": "Kimlik" + }, + "contactInfo": { + "message": "İletişim bilgileri" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Kart bilgileri" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index bcb552ee19b..fe9fde369ac 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Безпека" }, + "confirmMasterPassword": { + "message": "Підтвердьте головний пароль" + }, + "masterPassword": { + "message": "Головний пароль" + }, + "masterPassImportant": { + "message": "Головний пароль неможливо відновити, якщо ви його втратите!" + }, + "masterPassHintLabel": { + "message": "Підказка для головного пароля" + }, "errorOccurred": { "message": "Сталася помилка" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "Переглянути $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Історія паролів" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Необхідно підтвердити е-пошту" }, + "emailVerifiedV2": { + "message": "Електронну пошту підтверджено" + }, "emailVerificationRequiredDesc": { "message": "Для використання цієї функції необхідно підтвердити електронну пошту. Ви можете виконати підтвердження у вебсховищі." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Ключ доступу вилучено" }, - "unassignedItemsBannerNotice": { - "message": "Примітка: непризначені елементи організації більше не видимі у поданні \"Усі сховища\" і доступні лише в консолі адміністратора." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Примітка: 16 травня 2024 року непризначені елементи організації більше не будуть видимі у поданні \"Усі сховища\" і будуть доступні лише через консоль адміністратора." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Призначте ці елементи збірці в", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "щоб зробити їх видимими.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Пропозиції автозаповнення" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Елементи в деактивованих організаціях недоступні. Зверніться до власника вашої організації для отримання допомоги." }, + "additionalInformation": { + "message": "Додаткова інформація" + }, + "itemHistory": { + "message": "Історія запису" + }, + "lastEdited": { + "message": "Востаннє редаговано" + }, + "ownerYou": { + "message": "Власник: Ви" + }, + "linked": { + "message": "Пов'язано" + }, + "copySuccessful": { + "message": "Успішно скопійовано" + }, "upload": { "message": "Вивантажити" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Фільтри" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Завантажити – $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Подробиці картки" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index c63e571076c..3210c43acbf 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Bảo mật" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Đã xảy ra lỗi" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Lịch sử mật khẩu" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Yêu cầu xác nhận danh tính qua email" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Bạn phải xác nhận email để sử dụng tính năng này. Bạn có thể xác minh email trên web." }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Lưu ý: Các mục tổ chức chưa được chỉ định sẽ không còn hiển thị trong chế độ xem Tất cả Vault và chỉ có thể truy cập được qua Bảng điều khiển dành cho quản trị viên." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Lưu ý: Vào ngày 16 tháng 5 năm 2024, các mục tổ chức chưa được chỉ định sẽ không còn hiển thị trong chế độ xem Tất cả Vault và sẽ chỉ có thể truy cập được qua Bảng điều khiển dành cho quản trị viên." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Gán các mục này vào một bộ sưu tập từ", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "để làm cho chúng hiển thị.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 8bb4ad8e6f4..afc990ea68d 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -556,6 +556,18 @@ "security": { "message": "安全" }, + "confirmMasterPassword": { + "message": "确认主密码" + }, + "masterPassword": { + "message": "主密码" + }, + "masterPassImportant": { + "message": "主密码忘记后,将无法恢复!" + }, + "masterPassHintLabel": { + "message": "主密码提示" + }, "errorOccurred": { "message": "发生了一个错误" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "查看 $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "密码历史记录" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "需要验证电子邮件" }, + "emailVerifiedV2": { + "message": "电子邮箱已验证" + }, "emailVerificationRequiredDesc": { "message": "您必须验证电子邮件才能使用此功能。您可以在网页密码库中验证您的电子邮件。" }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "通行密钥已移除" }, - "unassignedItemsBannerNotice": { - "message": "注意:未分配的组织项目在「所有密码库」视图中不再可见,只能通过管理控制台访问。" - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "注意:从 2024 年 5 月 16 日起,未分配的组织项目在「所有密码库」视图中将不再可见,只能通过管理控制台访问。" - }, - "unassignedItemsBannerCTAPartOne": { - "message": "将这些项目分配到集合,通过", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ",以使其可见。", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "自动填充建议" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "无法访问已停用组织中的项目。请联系您的组织所有者获取协助。" }, + "additionalInformation": { + "message": "更多信息" + }, + "itemHistory": { + "message": "项目历史记录" + }, + "lastEdited": { + "message": "上次编辑" + }, + "ownerYou": { + "message": "所有者:您" + }, + "linked": { + "message": "已链接" + }, + "copySuccessful": { + "message": "复制成功" + }, "upload": { "message": "上传" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "筛选" }, + "personalDetails": { + "message": "个人信息" + }, + "identification": { + "message": "身份" + }, + "contactInfo": { + "message": "联系信息" + }, + "downloadAttachment": { + "message": "下载 - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "支付卡详情" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 446439ba2b6..37436357245 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -556,6 +556,18 @@ "security": { "message": "安全" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "發生錯誤" }, @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "密碼歷史記錄" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "需要驗證電子郵件" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "您必須驗證您的電子郵件才能使用此功能。您可以在網頁密碼庫裡驗證您的電子郵件。" }, @@ -3334,20 +3358,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3520,6 +3530,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3559,6 +3587,24 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +3616,8 @@ "example": "Visa" } } + }, + "addAccount": { + "message": "Add account" } } diff --git a/apps/browser/src/auth/popup/account-switching/account.component.html b/apps/browser/src/auth/popup/account-switching/account.component.html index 301a127a7d9..f1657ff3c26 100644 --- a/apps/browser/src/auth/popup/account-switching/account.component.html +++ b/apps/browser/src/auth/popup/account-switching/account.component.html @@ -49,6 +49,6 @@ >
- {{ account.name }} + {{ account.name | i18n }}
diff --git a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts index d60b0dfaebc..7cc9f8a92f2 100644 --- a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts +++ b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts @@ -80,7 +80,7 @@ export class AccountSwitcherService { if (!hasMaxAccounts) { options.push({ - name: "Add account", + name: "addAccount", id: this.SPECIAL_ADD_ACCOUNT_ID, isActive: false, }); diff --git a/apps/browser/src/auth/popup/two-factor-auth-email.component.ts b/apps/browser/src/auth/popup/two-factor-auth-email.component.ts new file mode 100644 index 00000000000..e865435b8b4 --- /dev/null +++ b/apps/browser/src/auth/popup/two-factor-auth-email.component.ts @@ -0,0 +1,55 @@ +import { DialogModule } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, inject } from "@angular/core"; +import { ReactiveFormsModule, FormsModule } from "@angular/forms"; + +import { TwoFactorAuthEmailComponent as TwoFactorAuthEmailBaseComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-email.component"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; + +import { AsyncActionsModule } from "../../../../../libs/components/src/async-actions"; +import { ButtonModule } from "../../../../../libs/components/src/button"; +import { DialogService } from "../../../../../libs/components/src/dialog"; +import { FormFieldModule } from "../../../../../libs/components/src/form-field"; +import { LinkModule } from "../../../../../libs/components/src/link"; +import { I18nPipe } from "../../../../../libs/components/src/shared/i18n.pipe"; +import { TypographyModule } from "../../../../../libs/components/src/typography"; +import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; + +@Component({ + standalone: true, + selector: "app-two-factor-auth-email", + templateUrl: + "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.html", + imports: [ + CommonModule, + JslibModule, + DialogModule, + ButtonModule, + LinkModule, + TypographyModule, + ReactiveFormsModule, + FormFieldModule, + AsyncActionsModule, + FormsModule, + ], + providers: [I18nPipe], +}) +export class TwoFactorAuthEmailComponent extends TwoFactorAuthEmailBaseComponent { + private dialogService = inject(DialogService); + + async ngOnInit(): Promise { + if (BrowserPopupUtils.inPopup(window)) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "warning" }, + content: { key: "popup2faCloseMessage" }, + type: "warning", + }); + if (confirmed) { + await BrowserPopupUtils.openCurrentPagePopout(window); + return; + } + } + + await super.ngOnInit(); + } +} diff --git a/apps/browser/src/auth/popup/two-factor-auth.component.ts b/apps/browser/src/auth/popup/two-factor-auth.component.ts index 67ff0fd2857..d2a1ba20bff 100644 --- a/apps/browser/src/auth/popup/two-factor-auth.component.ts +++ b/apps/browser/src/auth/popup/two-factor-auth.component.ts @@ -4,6 +4,7 @@ import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; import { ActivatedRoute, Router, RouterLink } from "@angular/router"; import { TwoFactorAuthAuthenticatorComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-authenticator.component"; +import { TwoFactorAuthWebAuthnComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-webauthn.component"; import { TwoFactorAuthYubikeyComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-yubikey.component"; import { TwoFactorAuthComponent as BaseTwoFactorAuthComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth.component"; import { TwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-options.component"; @@ -41,6 +42,8 @@ import { import { BrowserApi } from "../../platform/browser/browser-api"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; +import { TwoFactorAuthEmailComponent } from "./two-factor-auth-email.component"; + @Component({ standalone: true, templateUrl: @@ -59,8 +62,10 @@ import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; RouterLink, CheckboxModule, TwoFactorOptionsComponent, + TwoFactorAuthEmailComponent, TwoFactorAuthAuthenticatorComponent, TwoFactorAuthYubikeyComponent, + TwoFactorAuthWebAuthnComponent, ], providers: [I18nPipe], }) diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts index 98363bc93cc..f3c44ca9ca2 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -25,7 +25,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { BrowserApi } from "../../platform/browser/browser-api"; import { ZonedMessageListenerService } from "../../platform/browser/zoned-message-listener.service"; @@ -62,6 +62,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { private dialogService: DialogService, masterPasswordService: InternalMasterPasswordServiceAbstraction, accountService: AccountService, + toastService: ToastService, @Inject(WINDOW) protected win: Window, private browserMessagingApi: ZonedMessageListenerService, ) { @@ -84,6 +85,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { configService, masterPasswordService, accountService, + toastService, ); super.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -226,6 +228,15 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { } override async launchDuoFrameless() { + if (this.duoFramelessUrl === null) { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("duoHealthCheckResultsInNullAuthUrlError"), + }); + return; + } + const duoHandOffMessage = { title: this.i18nService.t("youSuccessfullyLoggedIn"), message: this.i18nService.t("youMayCloseThisWindow"), diff --git a/apps/browser/src/autofill/background/abstractions/overlay.background.ts b/apps/browser/src/autofill/background/abstractions/overlay.background.ts index aa62194af5c..462acb818b8 100644 --- a/apps/browser/src/autofill/background/abstractions/overlay.background.ts +++ b/apps/browser/src/autofill/background/abstractions/overlay.background.ts @@ -2,17 +2,43 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import AutofillPageDetails from "../../models/autofill-page-details"; +import { PageDetail } from "../../services/abstractions/autofill.service"; import { LockedVaultPendingNotificationsData } from "./notification.background"; -type WebsiteIconData = { +export type PageDetailsForTab = Record< + chrome.runtime.MessageSender["tab"]["id"], + Map +>; + +export type SubFrameOffsetData = { + top: number; + left: number; + url?: string; + frameId?: number; + parentFrameIds?: number[]; +} | null; + +export type SubFrameOffsetsForTab = Record< + chrome.runtime.MessageSender["tab"]["id"], + Map +>; + +export type WebsiteIconData = { imageEnabled: boolean; image: string; fallbackImage: string; icon: string; }; -type OverlayAddNewItemMessage = { +export type FocusedFieldData = { + focusedFieldStyles: Partial; + focusedFieldRects: Partial; + tabId?: number; + frameId?: number; +}; + +export type OverlayAddNewItemMessage = { login?: { uri?: string; hostname: string; @@ -21,112 +47,132 @@ type OverlayAddNewItemMessage = { }; }; -type OverlayBackgroundExtensionMessage = { - [key: string]: any; +export type CloseInlineMenuMessage = { + forceCloseInlineMenu?: boolean; + overlayElement?: string; +}; + +export type ToggleInlineMenuHiddenMessage = { + isInlineMenuHidden?: boolean; + setTransparentInlineMenu?: boolean; +}; + +export type OverlayBackgroundExtensionMessage = { command: string; + portKey?: string; tab?: chrome.tabs.Tab; sender?: string; details?: AutofillPageDetails; - overlayElement?: string; - display?: string; + isFieldCurrentlyFocused?: boolean; + isFieldCurrentlyFilling?: boolean; + isVisible?: boolean; + subFrameData?: SubFrameOffsetData; + focusedFieldData?: FocusedFieldData; + styles?: Partial; data?: LockedVaultPendingNotificationsData; -} & OverlayAddNewItemMessage; +} & OverlayAddNewItemMessage & + CloseInlineMenuMessage & + ToggleInlineMenuHiddenMessage; -type OverlayPortMessage = { +export type OverlayPortMessage = { [key: string]: any; command: string; direction?: string; - overlayCipherId?: string; + inlineMenuCipherId?: string; }; -type FocusedFieldData = { - focusedFieldStyles: Partial; - focusedFieldRects: Partial; - tabId?: number; -}; - -type OverlayCipherData = { +export type InlineMenuCipherData = { id: string; name: string; type: CipherType; reprompt: CipherRepromptType; favorite: boolean; - icon: { imageEnabled: boolean; image: string; fallbackImage: string; icon: string }; + icon: WebsiteIconData; login?: { username: string }; card?: string; }; -type BackgroundMessageParam = { +export type BackgroundMessageParam = { message: OverlayBackgroundExtensionMessage; }; -type BackgroundSenderParam = { +export type BackgroundSenderParam = { sender: chrome.runtime.MessageSender; }; -type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam; +export type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam; -type OverlayBackgroundExtensionMessageHandlers = { +export type OverlayBackgroundExtensionMessageHandlers = { [key: string]: CallableFunction; - openAutofillOverlay: () => void; autofillOverlayElementClosed: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; autofillOverlayAddNewVaultItem: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - getAutofillOverlayVisibility: () => void; - checkAutofillOverlayFocused: () => void; - focusAutofillOverlayList: () => void; - updateAutofillOverlayPosition: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - updateAutofillOverlayHidden: ({ message }: BackgroundMessageParam) => void; + triggerAutofillOverlayReposition: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + checkIsInlineMenuCiphersPopulated: ({ sender }: BackgroundSenderParam) => void; updateFocusedFieldData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + updateIsFieldCurrentlyFocused: ({ message }: BackgroundMessageParam) => void; + checkIsFieldCurrentlyFocused: () => boolean; + updateIsFieldCurrentlyFilling: ({ message }: BackgroundMessageParam) => void; + checkIsFieldCurrentlyFilling: () => boolean; + getAutofillInlineMenuVisibility: () => void; + openAutofillInlineMenu: () => void; + closeAutofillInlineMenu: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + checkAutofillInlineMenuFocused: ({ sender }: BackgroundSenderParam) => void; + focusAutofillInlineMenuList: () => void; + updateAutofillInlineMenuPosition: ({ + message, + sender, + }: BackgroundOnMessageHandlerParams) => Promise; + updateAutofillInlineMenuElementIsVisibleStatus: ({ + message, + sender, + }: BackgroundOnMessageHandlerParams) => void; + checkIsAutofillInlineMenuButtonVisible: () => void; + checkIsAutofillInlineMenuListVisible: () => void; + getCurrentTabFrameId: ({ sender }: BackgroundSenderParam) => number; + updateSubFrameData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + triggerSubFrameFocusInRebuild: ({ sender }: BackgroundSenderParam) => void; + destroyAutofillInlineMenuListeners: ({ + message, + sender, + }: BackgroundOnMessageHandlerParams) => void; collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; unlockCompleted: ({ message }: BackgroundMessageParam) => void; + doFullSync: () => void; addedCipher: () => void; addEditCipherSubmitted: () => void; editedCipher: () => void; deletedCipher: () => void; }; -type PortMessageParam = { +export type PortMessageParam = { message: OverlayPortMessage; }; -type PortConnectionParam = { +export type PortConnectionParam = { port: chrome.runtime.Port; }; -type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam; +export type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam; -type OverlayButtonPortMessageHandlers = { +export type InlineMenuButtonPortMessageHandlers = { [key: string]: CallableFunction; - overlayButtonClicked: ({ port }: PortConnectionParam) => void; - closeAutofillOverlay: ({ port }: PortConnectionParam) => void; - forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void; - overlayPageBlurred: () => void; - redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; + triggerDelayedAutofillInlineMenuClosure: ({ port }: PortConnectionParam) => void; + autofillInlineMenuButtonClicked: ({ port }: PortConnectionParam) => void; + autofillInlineMenuBlurred: () => void; + redirectAutofillInlineMenuFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; + updateAutofillInlineMenuColorScheme: () => void; }; -type OverlayListPortMessageHandlers = { +export type InlineMenuListPortMessageHandlers = { [key: string]: CallableFunction; - checkAutofillOverlayButtonFocused: () => void; - forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void; - overlayPageBlurred: () => void; + checkAutofillInlineMenuButtonFocused: () => void; + autofillInlineMenuBlurred: () => void; unlockVault: ({ port }: PortConnectionParam) => void; - fillSelectedListItem: ({ message, port }: PortOnMessageHandlerParams) => void; + fillAutofillInlineMenuCipher: ({ message, port }: PortOnMessageHandlerParams) => void; addNewVaultItem: ({ port }: PortConnectionParam) => void; viewSelectedCipher: ({ message, port }: PortOnMessageHandlerParams) => void; - redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; + redirectAutofillInlineMenuFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; + updateAutofillInlineMenuListHeight: ({ message, port }: PortOnMessageHandlerParams) => void; }; -interface OverlayBackground { +export interface OverlayBackground { init(): Promise; removePageDetails(tabId: number): void; - updateOverlayCiphers(): void; + updateOverlayCiphers(): Promise; } - -export { - WebsiteIconData, - OverlayBackgroundExtensionMessage, - OverlayPortMessage, - FocusedFieldData, - OverlayCipherData, - OverlayAddNewItemMessage, - OverlayBackgroundExtensionMessageHandlers, - OverlayButtonPortMessageHandlers, - OverlayListPortMessageHandlers, - OverlayBackground, -}; diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 179598a8823..9e989b73e62 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -770,12 +770,12 @@ export default class NotificationBackground { ) => { const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command]; if (!handler) { - return; + return null; } const messageResponse = handler({ message, sender }); - if (!messageResponse) { - return; + if (typeof messageResponse === "undefined") { + return null; } Promise.resolve(messageResponse) diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index 7be93b11e6b..81a7754f84b 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -1,102 +1,161 @@ import { mock, MockProxy, mockReset } from "jest-mock-extended"; -import { BehaviorSubject, of } from "rxjs"; +import { BehaviorSubject } from "rxjs"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { - SHOW_AUTOFILL_BUTTON, AutofillOverlayVisibility, + SHOW_AUTOFILL_BUTTON, } from "@bitwarden/common/autofill/constants"; -import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { AutofillSettingsServiceAbstraction as AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DefaultDomainSettingsService, DomainSettingsService, } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { EnvironmentService, Region, } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CloudEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; -import { I18nService } from "@bitwarden/common/platform/services/i18n.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { - FakeStateProvider, FakeAccountService, + FakeStateProvider, mockAccountServiceWith, } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; -import { CipherType } from "@bitwarden/common/vault/enums"; -import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { BrowserApi } from "../../platform/browser/browser-api"; import { BrowserPlatformUtilsService } from "../../platform/services/platform-utils/browser-platform-utils.service"; -import { AutofillService } from "../services/abstractions/autofill.service"; -import { - createAutofillPageDetailsMock, - createChromeTabMock, - createFocusedFieldDataMock, - createPageDetailMock, - createPortSpyMock, -} from "../spec/autofill-mocks"; -import { flushPromises, sendMockExtensionMessage, sendPortMessage } from "../spec/testing-utils"; import { AutofillOverlayElement, AutofillOverlayPort, + MAX_SUB_FRAME_DEPTH, RedirectFocusDirection, -} from "../utils/autofill-overlay.enum"; +} from "../enums/autofill-overlay.enum"; +import { AutofillService } from "../services/abstractions/autofill.service"; +import { + createChromeTabMock, + createAutofillPageDetailsMock, + createPortSpyMock, + createFocusedFieldDataMock, + createPageDetailMock, +} from "../spec/autofill-mocks"; +import { + flushPromises, + sendMockExtensionMessage, + sendPortMessage, + triggerPortOnConnectEvent, + triggerPortOnDisconnectEvent, + triggerPortOnMessageEvent, + triggerWebNavigationOnCommittedEvent, +} from "../spec/testing-utils"; -import OverlayBackground from "./overlay.background"; +import { + FocusedFieldData, + PageDetailsForTab, + SubFrameOffsetData, + SubFrameOffsetsForTab, +} from "./abstractions/overlay.background"; +import { OverlayBackground } from "./overlay.background"; describe("OverlayBackground", () => { const mockUserId = Utils.newGuid() as UserId; - const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); - const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService); + const sendResponse = jest.fn(); + let accountService: FakeAccountService; + let fakeStateProvider: FakeStateProvider; + let showFaviconsMock$: BehaviorSubject; let domainSettingsService: DomainSettingsService; - let buttonPortSpy: chrome.runtime.Port; - let listPortSpy: chrome.runtime.Port; - let overlayBackground: OverlayBackground; - const cipherService = mock(); - const autofillService = mock(); + let logService: MockProxy; + let cipherService: MockProxy; + let autofillService: MockProxy; let activeAccountStatusMock$: BehaviorSubject; let authService: MockProxy; + let environmentMock$: BehaviorSubject; + let environmentService: MockProxy; + let inlineMenuVisibilityMock$: BehaviorSubject; + let autofillSettingsService: MockProxy; + let i18nService: MockProxy; + let platformUtilsService: MockProxy; + let selectedThemeMock$: BehaviorSubject; + let themeStateService: MockProxy; + let overlayBackground: OverlayBackground; + let portKeyForTabSpy: Record; + let pageDetailsForTabSpy: PageDetailsForTab; + let subFrameOffsetsSpy: SubFrameOffsetsForTab; + let getFrameDetailsSpy: jest.SpyInstance; + let tabsSendMessageSpy: jest.SpyInstance; + let tabSendMessageDataSpy: jest.SpyInstance; + let sendMessageSpy: jest.SpyInstance; + let getTabFromCurrentWindowIdSpy: jest.SpyInstance; + let getTabSpy: jest.SpyInstance; + let openUnlockPopoutSpy: jest.SpyInstance; + let buttonPortSpy: chrome.runtime.Port; + let buttonMessageConnectorSpy: chrome.runtime.Port; + let listPortSpy: chrome.runtime.Port; + let listMessageConnectorSpy: chrome.runtime.Port; - const environmentService = mock(); - environmentService.environment$ = new BehaviorSubject( - new CloudEnvironment({ - key: Region.US, - domain: "bitwarden.com", - urls: { icons: "https://icons.bitwarden.com/" }, - }), - ); - const autofillSettingsService = mock(); - const i18nService = mock(); - const platformUtilsService = mock(); - const themeStateService = mock(); - const initOverlayElementPorts = async (options = { initList: true, initButton: true }) => { + let getFrameCounter: number = 2; + async function initOverlayElementPorts(options = { initList: true, initButton: true }) { const { initList, initButton } = options; if (initButton) { - await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.Button)); - buttonPortSpy = overlayBackground["overlayButtonPort"]; + triggerPortOnConnectEvent(createPortSpyMock(AutofillOverlayPort.Button)); + buttonPortSpy = overlayBackground["inlineMenuButtonPort"]; + + buttonMessageConnectorSpy = createPortSpyMock(AutofillOverlayPort.ButtonMessageConnector); + triggerPortOnConnectEvent(buttonMessageConnectorSpy); } if (initList) { - await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.List)); - listPortSpy = overlayBackground["overlayListPort"]; + triggerPortOnConnectEvent(createPortSpyMock(AutofillOverlayPort.List)); + listPortSpy = overlayBackground["inlineMenuListPort"]; + + listMessageConnectorSpy = createPortSpyMock(AutofillOverlayPort.ListMessageConnector); + triggerPortOnConnectEvent(listMessageConnectorSpy); } return { buttonPortSpy, listPortSpy }; - }; + } beforeEach(() => { + accountService = mockAccountServiceWith(mockUserId); + fakeStateProvider = new FakeStateProvider(accountService); + showFaviconsMock$ = new BehaviorSubject(true); domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); + domainSettingsService.showFavicons$ = showFaviconsMock$; + logService = mock(); + cipherService = mock(); + autofillService = mock(); activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked); authService = mock(); authService.activeAccountStatus$ = activeAccountStatusMock$; + environmentMock$ = new BehaviorSubject( + new CloudEnvironment({ + key: Region.US, + domain: "bitwarden.com", + urls: { icons: "https://icons.bitwarden.com/" }, + }), + ); + environmentService = mock(); + environmentService.environment$ = environmentMock$; + inlineMenuVisibilityMock$ = new BehaviorSubject(AutofillOverlayVisibility.OnFieldFocus); + autofillSettingsService = mock(); + autofillSettingsService.inlineMenuVisibility$ = inlineMenuVisibilityMock$; + i18nService = mock(); + platformUtilsService = mock(); + selectedThemeMock$ = new BehaviorSubject(ThemeType.Light); + themeStateService = mock(); + themeStateService.selectedTheme$ = selectedThemeMock$; overlayBackground = new OverlayBackground( + logService, cipherService, autofillService, authService, @@ -107,48 +166,528 @@ describe("OverlayBackground", () => { platformUtilsService, themeStateService, ); - - jest - .spyOn(overlayBackground as any, "getOverlayVisibility") - .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); - - themeStateService.selectedTheme$ = of(ThemeType.Light); - domainSettingsService.showFavicons$ = of(true); + portKeyForTabSpy = overlayBackground["portKeyForTab"]; + pageDetailsForTabSpy = overlayBackground["pageDetailsForTab"]; + subFrameOffsetsSpy = overlayBackground["subFrameOffsetsForTab"]; + getFrameDetailsSpy = jest.spyOn(BrowserApi, "getFrameDetails"); + getFrameDetailsSpy.mockImplementation((_details: chrome.webNavigation.GetFrameDetails) => { + getFrameCounter--; + return mock({ + parentFrameId: getFrameCounter, + }); + }); + tabsSendMessageSpy = jest.spyOn(BrowserApi, "tabSendMessage"); + tabSendMessageDataSpy = jest.spyOn(BrowserApi, "tabSendMessageData"); + sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage"); + getTabFromCurrentWindowIdSpy = jest.spyOn(BrowserApi, "getTabFromCurrentWindowId"); + getTabSpy = jest.spyOn(BrowserApi, "getTab"); + openUnlockPopoutSpy = jest.spyOn(overlayBackground as any, "openUnlockPopout"); void overlayBackground.init(); }); afterEach(() => { + getFrameCounter = 2; jest.clearAllMocks(); + jest.useRealTimers(); mockReset(cipherService); }); - describe("removePageDetails", () => { - it("removes the page details for a specific tab from the pageDetailsForTab object", () => { + describe("storing pageDetails", () => { + const tabId = 1; + + beforeEach(() => { + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ tab: createChromeTabMock({ id: tabId }), frameId: 0 }), + ); + }); + + it("stores the page details for the tab", () => { + expect(pageDetailsForTabSpy[tabId]).toBeDefined(); + }); + + describe("building sub frame offsets", () => { + beforeEach(() => { + tabsSendMessageSpy.mockResolvedValue( + mock({ + left: getFrameCounter, + top: getFrameCounter, + url: "url", + }), + ); + }); + + it("triggers a destruction of the inline menu listeners if the max frame depth is exceeded ", async () => { + getFrameCounter = MAX_SUB_FRAME_DEPTH + 1; + const tab = createChromeTabMock({ id: tabId }); + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ + tab, + frameId: 1, + }), + ); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + tab, + { command: "destroyAutofillInlineMenuListeners" }, + { frameId: 1 }, + ); + }); + + it("builds the offset values for a sub frame within the tab", async () => { + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ + tab: createChromeTabMock({ id: tabId }), + frameId: 1, + }), + ); + await flushPromises(); + + expect(subFrameOffsetsSpy[tabId]).toStrictEqual( + new Map([[1, { left: 4, top: 4, url: "url", parentFrameIds: [0, 1] }]]), + ); + expect(pageDetailsForTabSpy[tabId].size).toBe(2); + }); + + it("skips building offset values for a previously calculated sub frame", async () => { + getFrameCounter = 0; + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ + tab: createChromeTabMock({ id: tabId }), + frameId: 1, + }), + ); + await flushPromises(); + + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ + tab: createChromeTabMock({ id: tabId }), + frameId: 1, + }), + ); + await flushPromises(); + + expect(getFrameDetailsSpy).toHaveBeenCalledTimes(1); + expect(subFrameOffsetsSpy[tabId]).toStrictEqual( + new Map([[1, { left: 0, top: 0, url: "url", parentFrameIds: [0] }]]), + ); + }); + + it("will attempt to build the sub frame offsets by posting window messages if a set of offsets is not returned", async () => { + const tab = createChromeTabMock({ id: tabId }); + const frameId = 1; + tabsSendMessageSpy.mockResolvedValue(null); + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ tab, frameId }), + ); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + tab, + { + command: "getSubFrameOffsetsFromWindowMessage", + subFrameId: frameId, + }, + { frameId }, + ); + expect(subFrameOffsetsSpy[tabId]).toStrictEqual(new Map([[frameId, null]])); + }); + + it("updates sub frame data that has been calculated using window messages", async () => { + const tab = createChromeTabMock({ id: tabId }); + const frameId = 1; + const subFrameData = mock({ frameId, left: 10, top: 10, url: "url" }); + tabsSendMessageSpy.mockResolvedValueOnce(null); + subFrameOffsetsSpy[tabId] = new Map([[frameId, null]]); + + sendMockExtensionMessage( + { command: "updateSubFrameData", subFrameData }, + mock({ tab, frameId }), + ); + await flushPromises(); + + expect(subFrameOffsetsSpy[tabId]).toStrictEqual(new Map([[frameId, subFrameData]])); + }); + }); + }); + + describe("removing pageDetails", () => { + it("removes the page details and port key for a specific tab from the pageDetailsForTab object", () => { const tabId = 1; - const frameId = 2; - overlayBackground["pageDetailsForTab"][tabId] = new Map([[frameId, createPageDetailMock()]]); + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ tab: createChromeTabMock({ id: tabId }), frameId: 1 }), + ); + overlayBackground.removePageDetails(tabId); - expect(overlayBackground["pageDetailsForTab"][tabId]).toBeUndefined(); + expect(pageDetailsForTabSpy[tabId]).toBeUndefined(); + expect(portKeyForTabSpy[tabId]).toBeUndefined(); }); }); - describe("init", () => { - it("sets up the extension message listeners, get the overlay's visibility settings, and get the user's auth status", async () => { - overlayBackground["setupExtensionMessageListeners"] = jest.fn(); - overlayBackground["getOverlayVisibility"] = jest.fn(); - overlayBackground["getAuthStatus"] = jest.fn(); + describe("re-positioning the inline menu within sub frames", () => { + const tabId = 1; + const topFrameId = 0; + const middleFrameId = 10; + const middleAdjacentFrameId = 11; + const bottomFrameId = 20; + let tab: chrome.tabs.Tab; + let sender: MockProxy; - await overlayBackground.init(); + async function flushOverlayRepositionPromises() { + await flushPromises(); + jest.advanceTimersByTime(1150); + await flushPromises(); + } - expect(overlayBackground["setupExtensionMessageListeners"]).toHaveBeenCalled(); - expect(overlayBackground["getOverlayVisibility"]).toHaveBeenCalled(); - expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); + beforeEach(() => { + jest.useFakeTimers(); + tab = createChromeTabMock({ id: tabId }); + sender = mock({ tab, frameId: middleFrameId }); + overlayBackground["focusedFieldData"] = mock({ + tabId, + frameId: bottomFrameId, + }); + subFrameOffsetsSpy[tabId] = new Map([ + [topFrameId, { left: 1, top: 1, url: "https://top-frame.com", parentFrameIds: [] }], + [ + middleFrameId, + { left: 2, top: 2, url: "https://middle-frame.com", parentFrameIds: [topFrameId] }, + ], + [ + middleAdjacentFrameId, + { + left: 3, + top: 3, + url: "https://middle-adjacent-frame.com", + parentFrameIds: [topFrameId], + }, + ], + [ + bottomFrameId, + { + left: 4, + top: 4, + url: "https://bottom-frame.com", + parentFrameIds: [topFrameId, middleFrameId], + }, + ], + ]); + tabsSendMessageSpy.mockResolvedValue( + mock({ + left: getFrameCounter, + top: getFrameCounter, + url: "url", + }), + ); + }); + + describe("triggerAutofillOverlayReposition", () => { + describe("checkShouldRepositionInlineMenu", () => { + let focusedFieldData: FocusedFieldData; + let repositionInlineMenuSpy: jest.SpyInstance; + + beforeEach(() => { + focusedFieldData = createFocusedFieldDataMock({ tabId }); + repositionInlineMenuSpy = jest.spyOn(overlayBackground as any, "repositionInlineMenu"); + }); + + describe("blocking a reposition of the overlay", () => { + it("blocks repositioning when the focused field data is not set", async () => { + overlayBackground["focusedFieldData"] = undefined; + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushOverlayRepositionPromises(); + + expect(repositionInlineMenuSpy).not.toHaveBeenCalled(); + }); + + it("blocks repositioning when the sender is from a different tab than the focused field", async () => { + const otherSender = mock({ frameId: 1, tab: { id: 2 } }); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData }, + otherSender, + ); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushOverlayRepositionPromises(); + + expect(repositionInlineMenuSpy).not.toHaveBeenCalled(); + }); + + it("blocks repositioning when the sender frame is not a parent frame of the focused field", async () => { + focusedFieldData = createFocusedFieldDataMock({ tabId }); + const otherFrameSender = mock({ + tab, + frameId: middleAdjacentFrameId, + }); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData }, + otherFrameSender, + ); + sender.frameId = bottomFrameId; + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushOverlayRepositionPromises(); + + expect(repositionInlineMenuSpy).not.toHaveBeenCalled(); + }); + }); + + describe("allowing a reposition of the overlay", () => { + it("allows repositioning when the sender frame is for the focused field and the inline menu is visible, ", async () => { + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData }, + sender, + ); + tabsSendMessageSpy.mockImplementation((_tab, message) => { + if (message.command === "checkIsAutofillInlineMenuButtonVisible") { + return Promise.resolve(true); + } + }); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushOverlayRepositionPromises(); + + expect(repositionInlineMenuSpy).toHaveBeenCalled(); + }); + }); + }); + + describe("repositionInlineMenu", () => { + beforeEach(() => { + overlayBackground["isFieldCurrentlyFocused"] = true; + }); + + it("closes the inline menu if the field is not focused", async () => { + overlayBackground["isFieldCurrentlyFocused"] = false; + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushOverlayRepositionPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + tab, + { command: "closeAutofillInlineMenu" }, + { frameId: 0 }, + ); + }); + + it("closes the inline menu if the focused field is not within the viewport", async () => { + tabsSendMessageSpy.mockImplementation((_tab, message) => { + if (message.command === "checkIsMostRecentlyFocusedFieldWithinViewport") { + return Promise.resolve(false); + } + }); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushOverlayRepositionPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + tab, + { command: "closeAutofillInlineMenu" }, + { frameId: 0 }, + ); + }); + + it("rebuilds the sub frame offsets when the focused field's frame id indicates that it is within a sub frame", async () => { + const focusedFieldData = createFocusedFieldDataMock({ tabId, frameId: middleFrameId }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushOverlayRepositionPromises(); + + expect(getFrameDetailsSpy).toHaveBeenCalledWith({ tabId, frameId: middleFrameId }); + }); + + describe("updating the inline menu position", () => { + let sender: chrome.runtime.MessageSender; + + async function flushUpdateInlineMenuPromises() { + await flushOverlayRepositionPromises(); + await flushPromises(); + jest.advanceTimersByTime(250); + await flushPromises(); + } + + beforeEach(async () => { + sender = mock({ tab, frameId: middleFrameId }); + jest.useFakeTimers(); + await initOverlayElementPorts(); + }); + + it("skips updating the position of either inline menu element if a field is not currently focused", async () => { + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFocused", + isFieldCurrentlyFocused: false, + }); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushUpdateInlineMenuPromises(); + + expect(tabsSendMessageSpy).not.toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.Button, + }, + { frameId: 0 }, + ); + expect(tabsSendMessageSpy).not.toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.List, + }, + { frameId: 0 }, + ); + }); + + it("sets the inline menu invisible and updates its position", async () => { + overlayBackground["checkIsInlineMenuButtonVisible"] = jest + .fn() + .mockResolvedValue(false); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushUpdateInlineMenuPromises(); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "toggleAutofillInlineMenuHidden", + styles: { display: "none" }, + }); + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.Button, + }, + { frameId: 0 }, + ); + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.List, + }, + { frameId: 0 }, + ); + }); + + it("skips updating the inline menu list if the user has the inline menu set to open on button click", async () => { + inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnButtonClick); + tabsSendMessageSpy.mockImplementation((_tab, message, _options) => { + if (message.command === "checkMostRecentlyFocusedFieldHasValue") { + return Promise.resolve(true); + } + + return Promise.resolve({}); + }); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushUpdateInlineMenuPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.Button, + }, + { frameId: 0 }, + ); + expect(tabsSendMessageSpy).not.toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.List, + }, + { frameId: 0 }, + ); + }); + + it("skips updating the inline menu list if the focused field has a value and the user status is not unlocked", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Locked); + tabsSendMessageSpy.mockImplementation((_tab, message, _options) => { + if (message.command === "checkMostRecentlyFocusedFieldHasValue") { + return Promise.resolve(true); + } + + return Promise.resolve({}); + }); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushUpdateInlineMenuPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.Button, + }, + { frameId: 0 }, + ); + expect(tabsSendMessageSpy).not.toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.List, + }, + { frameId: 0 }, + ); + }); + }); + }); + + describe("triggerSubFrameFocusInRebuild", () => { + it("triggers a rebuild of the sub frame and updates the inline menu position", async () => { + const rebuildSubFrameOffsetsSpy = jest.spyOn( + overlayBackground as any, + "rebuildSubFrameOffsets", + ); + const repositionInlineMenuSpy = jest.spyOn( + overlayBackground as any, + "repositionInlineMenu", + ); + + sendMockExtensionMessage({ command: "triggerSubFrameFocusInRebuild" }, sender); + await flushOverlayRepositionPromises(); + + expect(rebuildSubFrameOffsetsSpy).toHaveBeenCalled(); + expect(repositionInlineMenuSpy).toHaveBeenCalled(); + }); + }); + + describe("toggleInlineMenuHidden", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + }); + + it("skips adjusting the hidden status of the inline menu if the sender tab does not contain the focused field", async () => { + const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + const otherSender = mock({ tab: { id: 2 } }); + + await overlayBackground["toggleInlineMenuHidden"]( + { isInlineMenuHidden: true }, + otherSender, + ); + + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "toggleAutofillInlineMenuHidden", + styles: { display: "none" }, + }); + }); + }); }); }); - describe("updateOverlayCiphers", () => { + describe("updating the overlay ciphers", () => { const url = "https://jest-testing-website.com"; const tab = createChromeTabMock({ url }); const cipher1 = mock({ @@ -160,86 +699,100 @@ describe("OverlayBackground", () => { }); const cipher2 = mock({ id: "id-2", - localData: { lastUsedDate: 111 }, + localData: { lastUsedDate: 222 }, name: "name-2", - type: CipherType.Login, - login: { username: "username-2", uri: url }, + type: CipherType.Card, + card: { subTitle: "subtitle-2" }, }); beforeEach(() => { activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); }); - it("ignores updating the overlay ciphers if the user's auth status is not unlocked", async () => { + it("skips updating the overlay ciphers if the user's auth status is not unlocked", async () => { activeAccountStatusMock$.next(AuthenticationStatus.Locked); - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId"); - jest.spyOn(cipherService, "getAllDecryptedForUrl"); await overlayBackground.updateOverlayCiphers(); - expect(BrowserApi.getTabFromCurrentWindowId).not.toHaveBeenCalled(); + expect(getTabFromCurrentWindowIdSpy).not.toHaveBeenCalled(); expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled(); }); - it("ignores updating the overlay ciphers if the tab is undefined", async () => { - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(undefined); - jest.spyOn(cipherService, "getAllDecryptedForUrl"); + it("closes the inline menu on the focused field's tab if the user's auth status is not unlocked", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Locked); + const previousTab = mock({ id: 1 }); + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: 1 }); + getTabSpy.mockResolvedValueOnce(previousTab); await overlayBackground.updateOverlayCiphers(); - expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); - expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled(); + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + previousTab, + { command: "closeAutofillInlineMenu", overlayElement: undefined }, + { frameId: 0 }, + ); + }); + + it("closes the inline menu on the focused field's tab if current tab is different", async () => { + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); + cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); + cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); + const previousTab = mock({ id: 15 }); + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: 15 }); + getTabSpy.mockResolvedValueOnce(previousTab); + + await overlayBackground.updateOverlayCiphers(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + previousTab, + { command: "closeAutofillInlineMenu", overlayElement: undefined }, + { frameId: 0 }, + ); }); it("queries all ciphers for the given url, sort them by last used, and format them for usage in the overlay", async () => { - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(tab); + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); - jest.spyOn(overlayBackground as any, "getOverlayCipherData"); await overlayBackground.updateOverlayCiphers(); expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url); - expect(overlayBackground["cipherService"].sortCiphersByLastUsedThenName).toHaveBeenCalled(); - expect(overlayBackground["overlayLoginCiphers"]).toStrictEqual( + expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled(); + expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual( new Map([ - ["overlay-cipher-0", cipher2], - ["overlay-cipher-1", cipher1], + ["inline-menu-cipher-0", cipher2], + ["inline-menu-cipher-1", cipher1], ]), ); - expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled(); }); - it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateIsOverlayCiphersPopulated` message to the tab indicating that the list of ciphers is populated", async () => { - overlayBackground["overlayListPort"] = mock(); + it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateAutofillInlineMenuListCiphers` message to the tab indicating that the list of ciphers is populated", async () => { + overlayBackground["inlineMenuListPort"] = mock(); cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(tab); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); await overlayBackground.updateOverlayCiphers(); - expect(overlayBackground["overlayListPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayListCiphers", + expect(overlayBackground["inlineMenuListPort"].postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuListCiphers", ciphers: [ { - card: null, + card: cipher2.card.subTitle, favorite: cipher2.favorite, icon: { - fallbackImage: "images/bwi-globe.png", - icon: "bwi-globe", - image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", + fallbackImage: "", + icon: "bwi-credit-card", + image: undefined, imageEnabled: true, }, - id: "overlay-cipher-0", - login: { - username: "username-2", - }, + id: "inline-menu-cipher-0", + login: null, name: "name-2", reprompt: cipher2.reprompt, - type: 1, + type: 3, }, { card: null, @@ -250,7 +803,7 @@ describe("OverlayBackground", () => { image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", imageEnabled: true, }, - id: "overlay-cipher-1", + id: "inline-menu-cipher-1", login: { username: "username-1", }, @@ -260,227 +813,822 @@ describe("OverlayBackground", () => { }, ], }); - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - tab, - "updateIsOverlayCiphersPopulated", - { isOverlayCiphersPopulated: true }, - ); }); }); - describe("getOverlayCipherData", () => { - const url = "https://jest-testing-website.com"; - const cipher1 = mock({ - id: "id-1", - localData: { lastUsedDate: 222 }, - name: "name-1", - type: CipherType.Login, - login: { username: "username-1", uri: url }, - }); - const cipher2 = mock({ - id: "id-2", - localData: { lastUsedDate: 111 }, - name: "name-2", - type: CipherType.Login, - login: { username: "username-2", uri: url }, - }); - const cipher3 = mock({ - id: "id-3", - localData: { lastUsedDate: 333 }, - name: "name-3", - type: CipherType.Card, - card: { subTitle: "Visa, *6789" }, - }); - const cipher4 = mock({ - id: "id-4", - localData: { lastUsedDate: 444 }, - name: "name-4", - type: CipherType.Card, - card: { subTitle: "Mastercard, *1234" }, - }); + describe("extension message handlers", () => { + describe("autofillOverlayElementClosed message handler", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + }); - it("formats and returns the cipher data", async () => { - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-0", cipher2], - ["overlay-cipher-1", cipher1], - ["overlay-cipher-2", cipher3], - ["overlay-cipher-3", cipher4], - ]); + it("disconnects any expired ports if the sender is not from the same page as the most recently focused field", () => { + const port1 = mock(); + const port2 = mock(); + overlayBackground["expiredPorts"] = [port1, port2]; + const sender = mock({ tab: { id: 1 } }); + const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - const overlayCipherData = await overlayBackground["getOverlayCipherData"](); - - expect(overlayCipherData).toStrictEqual([ - { - card: null, - favorite: cipher2.favorite, - icon: { - fallbackImage: "images/bwi-globe.png", - icon: "bwi-globe", - image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", - imageEnabled: true, + sendMockExtensionMessage( + { + command: "autofillOverlayElementClosed", + overlayElement: AutofillOverlayElement.Button, }, - id: "overlay-cipher-0", - login: { - username: "username-2", - }, - name: "name-2", - reprompt: cipher2.reprompt, - type: 1, - }, - { - card: null, - favorite: cipher1.favorite, - icon: { - fallbackImage: "images/bwi-globe.png", - icon: "bwi-globe", - image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", - imageEnabled: true, - }, - id: "overlay-cipher-1", - login: { - username: "username-1", - }, - name: "name-1", - reprompt: cipher1.reprompt, - type: 1, - }, - { - card: "Visa, *6789", - favorite: cipher3.favorite, - icon: { - fallbackImage: "", - icon: "bwi-credit-card", - image: undefined, - imageEnabled: true, - }, - id: "overlay-cipher-2", - login: null, - name: "name-3", - reprompt: cipher3.reprompt, - type: 3, - }, - { - card: "Mastercard, *1234", - favorite: cipher4.favorite, - icon: { - fallbackImage: "", - icon: "bwi-credit-card", - image: undefined, - imageEnabled: true, - }, - id: "overlay-cipher-3", - login: null, - name: "name-4", - reprompt: cipher4.reprompt, - type: 3, - }, - ]); - }); - }); + sender, + ); - describe("getAuthStatus", () => { - it("will update the user's auth status but will not update the overlay ciphers", async () => { - const authStatus = AuthenticationStatus.Unlocked; - overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; - jest.spyOn(overlayBackground["authService"], "getAuthStatus").mockResolvedValue(authStatus); - jest.spyOn(overlayBackground as any, "updateOverlayButtonAuthStatus").mockImplementation(); - jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation(); + expect(port1.disconnect).toHaveBeenCalled(); + expect(port2.disconnect).toHaveBeenCalled(); + }); - const status = await overlayBackground["getAuthStatus"](); + it("disconnects the button element port", () => { + sendMockExtensionMessage({ + command: "autofillOverlayElementClosed", + overlayElement: AutofillOverlayElement.Button, + }); - expect(overlayBackground["authService"].getAuthStatus).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayButtonAuthStatus"]).not.toHaveBeenCalled(); - expect(overlayBackground["updateOverlayCiphers"]).not.toHaveBeenCalled(); - expect(overlayBackground["userAuthStatus"]).toBe(authStatus); - expect(status).toBe(authStatus); - }); + expect(buttonPortSpy.disconnect).toHaveBeenCalled(); + }); - it("will update the user's auth status and update the overlay ciphers if the status has been modified", async () => { - const authStatus = AuthenticationStatus.Unlocked; - overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; - jest.spyOn(overlayBackground["authService"], "getAuthStatus").mockResolvedValue(authStatus); - jest.spyOn(overlayBackground as any, "updateOverlayButtonAuthStatus").mockImplementation(); - jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation(); + it("disconnects the list element port", () => { + sendMockExtensionMessage({ + command: "autofillOverlayElementClosed", + overlayElement: AutofillOverlayElement.List, + }); - await overlayBackground["getAuthStatus"](); - - expect(overlayBackground["authService"].getAuthStatus).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayButtonAuthStatus"]).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayCiphers"]).toHaveBeenCalled(); - expect(overlayBackground["userAuthStatus"]).toBe(authStatus); - }); - }); - - describe("updateOverlayButtonAuthStatus", () => { - it("will send a message to the button port with the user's auth status", () => { - overlayBackground["overlayButtonPort"] = mock(); - jest.spyOn(overlayBackground["overlayButtonPort"], "postMessage"); - - overlayBackground["updateOverlayButtonAuthStatus"](); - - expect(overlayBackground["overlayButtonPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayButtonAuthStatus", - authStatus: overlayBackground["userAuthStatus"], + expect(listPortSpy.disconnect).toHaveBeenCalled(); }); }); - }); - describe("getTranslations", () => { - it("will query the overlay page translations if they have not been queried", () => { - overlayBackground["overlayPageTranslations"] = undefined; - jest.spyOn(overlayBackground as any, "getTranslations"); - jest.spyOn(overlayBackground["i18nService"], "translate").mockImplementation((key) => key); - jest.spyOn(BrowserApi, "getUILanguage").mockReturnValue("en"); + describe("autofillOverlayAddNewVaultItem message handler", () => { + let sender: chrome.runtime.MessageSender; + let openAddEditVaultItemPopoutSpy: jest.SpyInstance; - const translations = overlayBackground["getTranslations"](); + beforeEach(() => { + sender = mock({ tab: { id: 1 } }); + openAddEditVaultItemPopoutSpy = jest + .spyOn(overlayBackground as any, "openAddEditVaultItemPopout") + .mockImplementation(); + }); - expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); - const translationKeys = [ - "opensInANewWindow", - "bitwardenOverlayButton", - "toggleBitwardenVaultOverlay", - "bitwardenVault", - "unlockYourAccountToViewMatchingLogins", - "unlockAccount", - "fillCredentialsFor", - "partialUsername", - "view", - "noItemsToShow", - "newItem", - "addNewVaultItem", + it("will not open the add edit popout window if the message does not have a login cipher provided", () => { + sendMockExtensionMessage({ command: "autofillOverlayAddNewVaultItem" }, sender); + + expect(cipherService.setAddEditCipherInfo).not.toHaveBeenCalled(); + expect(openAddEditVaultItemPopoutSpy).not.toHaveBeenCalled(); + }); + + it("will open the add edit popout window after creating a new cipher", async () => { + sendMockExtensionMessage( + { + command: "autofillOverlayAddNewVaultItem", + login: { + uri: "https://tacos.com", + hostname: "", + username: "username", + password: "password", + }, + }, + sender, + ); + await flushPromises(); + + expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); + expect(sendMessageSpy).toHaveBeenCalledWith("inlineAutofillMenuRefreshAddEditCipher"); + expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalled(); + }); + }); + + describe("checkIsInlineMenuCiphersPopulated message handler", () => { + let focusedFieldData: FocusedFieldData; + + beforeEach(() => { + focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData }, + mock({ tab: { id: 2 }, frameId: 0 }), + ); + }); + + it("returns false if the sender's tab id is not equal to the focused field's tab id", async () => { + const sender = mock({ tab: { id: 1 } }); + + sendMockExtensionMessage( + { command: "checkIsInlineMenuCiphersPopulated" }, + sender, + sendResponse, + ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(false); + }); + + it("returns false if the overlay login cipher are not populated", () => {}); + + it("returns true if the overlay login ciphers are populated", async () => { + overlayBackground["inlineMenuCiphers"] = new Map([ + ["inline-menu-cipher-0", mock()], + ]); + + sendMockExtensionMessage( + { command: "checkIsInlineMenuCiphersPopulated" }, + mock({ tab: { id: 2 } }), + sendResponse, + ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(true); + }); + }); + + describe("updateFocusedFieldData message handler", () => { + it("sends a message to the sender frame to unset the most recently focused field data when the currently focused field does not belong to the sender", async () => { + const tab = createChromeTabMock({ id: 2 }); + const firstSender = mock({ tab, frameId: 100 }); + const focusedFieldData = createFocusedFieldDataMock({ + tabId: tab.id, + frameId: firstSender.frameId, + }); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData }, + firstSender, + ); + await flushPromises(); + + const secondSender = mock({ tab, frameId: 10 }); + const otherFocusedFieldData = createFocusedFieldDataMock({ + tabId: tab.id, + frameId: secondSender.frameId, + }); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData: otherFocusedFieldData }, + secondSender, + ); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + tab, + { command: "unsetMostRecentlyFocusedField" }, + { frameId: firstSender.frameId }, + ); + }); + }); + + describe("checkIsFieldCurrentlyFocused message handler", () => { + it("returns true when a form field is currently focused", async () => { + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFocused", + isFieldCurrentlyFocused: true, + }); + + sendMockExtensionMessage( + { command: "checkIsFieldCurrentlyFocused" }, + mock(), + sendResponse, + ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(true); + }); + }); + + describe("checkIsFieldCurrentlyFilling message handler", () => { + it("returns true if autofill is currently running", async () => { + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFilling", + isFieldCurrentlyFilling: true, + }); + + sendMockExtensionMessage( + { command: "checkIsFieldCurrentlyFilling" }, + mock(), + sendResponse, + ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(true); + }); + }); + + describe("getAutofillInlineMenuVisibility message handler", () => { + it("returns the current inline menu visibility setting", async () => { + sendMockExtensionMessage( + { command: "getAutofillInlineMenuVisibility" }, + mock(), + sendResponse, + ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(AutofillOverlayVisibility.OnFieldFocus); + }); + }); + + describe("openAutofillInlineMenu message handler", () => { + let sender: chrome.runtime.MessageSender; + + beforeEach(() => { + sender = mock({ tab: { id: 1 } }); + getTabFromCurrentWindowIdSpy.mockResolvedValue(sender.tab); + tabsSendMessageSpy.mockImplementation(); + }); + + it("opens the autofill inline menu by sending a message to the current tab", async () => { + sendMockExtensionMessage({ command: "openAutofillInlineMenu" }, sender); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "openAutofillInlineMenu", + isFocusingFieldElement: false, + isOpeningFullInlineMenu: false, + authStatus: AuthenticationStatus.Unlocked, + }, + { frameId: 0 }, + ); + }); + + it("sends the open menu message to the focused field's frameId", async () => { + sender.frameId = 10; + sendMockExtensionMessage({ command: "updateFocusedFieldData" }, sender); + await flushPromises(); + + sendMockExtensionMessage({ command: "openAutofillInlineMenu" }, sender); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "openAutofillInlineMenu", + isFocusingFieldElement: false, + isOpeningFullInlineMenu: false, + authStatus: AuthenticationStatus.Unlocked, + }, + { frameId: 10 }, + ); + }); + }); + + describe("closeAutofillInlineMenu", () => { + let sender: chrome.runtime.MessageSender; + + beforeEach(() => { + sender = mock({ tab: { id: 1 } }); + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFilling", + isFieldCurrentlyFilling: false, + }); + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFocused", + isFieldCurrentlyFocused: false, + }); + }); + + it("sends a message to close the inline menu without checking field focus state if forcing the closure", async () => { + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFocused", + isFieldCurrentlyFocused: true, + }); + await flushPromises(); + + sendMockExtensionMessage( + { + command: "closeAutofillInlineMenu", + forceCloseInlineMenu: true, + overlayElement: AutofillOverlayElement.Button, + }, + sender, + ); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "closeAutofillInlineMenu", + overlayElement: AutofillOverlayElement.Button, + }, + { frameId: 0 }, + ); + }); + + it("skips sending a message to close the inline menu if a form field is currently focused", async () => { + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFocused", + isFieldCurrentlyFocused: true, + }); + await flushPromises(); + + sendMockExtensionMessage( + { + command: "closeAutofillInlineMenu", + forceCloseInlineMenu: false, + overlayElement: AutofillOverlayElement.Button, + }, + sender, + ); + await flushPromises(); + + expect(tabsSendMessageSpy).not.toHaveBeenCalled(); + }); + + it("sends a message to close the inline menu list only if the field is currently filling", async () => { + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFilling", + isFieldCurrentlyFilling: true, + }); + await flushPromises(); + + sendMockExtensionMessage({ command: "closeAutofillInlineMenu" }, sender); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "closeAutofillInlineMenu", + overlayElement: AutofillOverlayElement.List, + }, + { frameId: 0 }, + ); + expect(tabsSendMessageSpy).not.toHaveBeenCalledWith( + sender.tab, + { + command: "closeAutofillInlineMenu", + overlayElement: AutofillOverlayElement.Button, + }, + { frameId: 0 }, + ); + }); + + it("sends a message to close the inline menu if the form field is not focused and not filling", async () => { + overlayBackground["isInlineMenuButtonVisible"] = true; + overlayBackground["isInlineMenuListVisible"] = true; + + sendMockExtensionMessage({ command: "closeAutofillInlineMenu" }, sender); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "closeAutofillInlineMenu", + overlayElement: undefined, + }, + { frameId: 0 }, + ); + expect(overlayBackground["isInlineMenuButtonVisible"]).toBe(false); + expect(overlayBackground["isInlineMenuListVisible"]).toBe(false); + }); + + it("sets a property indicating that the inline menu button is not visible", async () => { + overlayBackground["isInlineMenuButtonVisible"] = true; + + sendMockExtensionMessage( + { command: "closeAutofillInlineMenu", overlayElement: AutofillOverlayElement.Button }, + sender, + ); + await flushPromises(); + + expect(overlayBackground["isInlineMenuButtonVisible"]).toBe(false); + }); + + it("sets a property indicating that the inline menu list is not visible", async () => { + overlayBackground["isInlineMenuListVisible"] = true; + + sendMockExtensionMessage( + { command: "closeAutofillInlineMenu", overlayElement: AutofillOverlayElement.List }, + sender, + ); + await flushPromises(); + + expect(overlayBackground["isInlineMenuListVisible"]).toBe(false); + }); + }); + + describe("checkAutofillInlineMenuFocused message handler", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + }); + + it("skips checking if the inline menu is focused if the sender does not contain the focused field", async () => { + const sender = mock({ tab: { id: 1 } }); + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ command: "checkAutofillInlineMenuFocused" }, sender); + + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuListFocused", + }); + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuButtonFocused", + }); + }); + + it("will check if the inline menu list is focused if the list port is open", () => { + sendMockExtensionMessage({ command: "checkAutofillInlineMenuFocused" }); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuListFocused", + }); + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuButtonFocused", + }); + }); + + it("will check if the overlay button is focused if the list port is not open", () => { + overlayBackground["inlineMenuListPort"] = undefined; + + sendMockExtensionMessage({ command: "checkAutofillInlineMenuFocused" }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuButtonFocused", + }); + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuListFocused", + }); + }); + }); + + describe("focusAutofillInlineMenuList message handler", () => { + it("will send a `focusInlineMenuList` message to the overlay list port", async () => { + await initOverlayElementPorts({ initList: true, initButton: false }); + + sendMockExtensionMessage({ command: "focusAutofillInlineMenuList" }); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "focusAutofillInlineMenuList", + }); + }); + }); + + describe("updateAutofillInlineMenuPosition message handler", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + }); + + it("ignores updating the position if the overlay element type is not provided", () => { + sendMockExtensionMessage({ command: "updateAutofillInlineMenuPosition" }); + + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + }); + + it("skips updating the position if the most recently focused field is different than the message sender", () => { + const sender = mock({ tab: { id: 1 } }); + const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ command: "updateAutofillInlineMenuPosition" }, sender); + + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + }); + + it("updates the inline menu button's position", async () => { + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillInlineMenuPosition", + overlayElement: AutofillOverlayElement.Button, + }); + await flushPromises(); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuPosition", + styles: { height: "2px", left: "4px", top: "2px", width: "2px" }, + }); + }); + + it("modifies the inline menu button's height for medium sized input elements", async () => { + const focusedFieldData = createFocusedFieldDataMock({ + focusedFieldRects: { top: 1, left: 2, height: 35, width: 4 }, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillInlineMenuPosition", + overlayElement: AutofillOverlayElement.Button, + }); + await flushPromises(); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuPosition", + styles: { height: "20px", left: "-22px", top: "8px", width: "20px" }, + }); + }); + + it("modifies the inline menu button's height for large sized input elements", async () => { + const focusedFieldData = createFocusedFieldDataMock({ + focusedFieldRects: { top: 1, left: 2, height: 50, width: 4 }, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillInlineMenuPosition", + overlayElement: AutofillOverlayElement.Button, + }); + await flushPromises(); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuPosition", + styles: { height: "27px", left: "-32px", top: "13px", width: "27px" }, + }); + }); + + it("takes into account the right padding of the focused field in positioning the button if the right padding of the field is larger than the left padding", async () => { + const focusedFieldData = createFocusedFieldDataMock({ + focusedFieldStyles: { paddingRight: "20px", paddingLeft: "6px" }, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillInlineMenuPosition", + overlayElement: AutofillOverlayElement.Button, + }); + await flushPromises(); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuPosition", + styles: { height: "2px", left: "-18px", top: "2px", width: "2px" }, + }); + }); + + it("updates the inline menu list's position", async () => { + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillInlineMenuPosition", + overlayElement: AutofillOverlayElement.List, + }); + await flushPromises(); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuPosition", + styles: { left: "2px", top: "4px", width: "4px" }, + }); + }); + + it("sends a message that triggers a simultaneous fade in for both inline menu elements", async () => { + jest.useFakeTimers(); + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillInlineMenuPosition", + overlayElement: AutofillOverlayElement.List, + }); + await flushPromises(); + jest.advanceTimersByTime(150); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "fadeInAutofillInlineMenuIframe", + }); + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "fadeInAutofillInlineMenuIframe", + }); + }); + + it("triggers a debounced reposition of the inline menu if the sender frame has a `null` sub frame offsets value", async () => { + jest.useFakeTimers(); + const focusedFieldData = createFocusedFieldDataMock(); + const sender = mock({ + tab: { id: focusedFieldData.tabId }, + frameId: focusedFieldData.frameId, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + overlayBackground["subFrameOffsetsForTab"][focusedFieldData.tabId] = new Map([ + [focusedFieldData.frameId, null], + ]); + tabsSendMessageSpy.mockImplementation(); + jest.spyOn(overlayBackground as any, "updateInlineMenuPositionAfterRepositionEvent"); + + sendMockExtensionMessage( + { + command: "updateAutofillInlineMenuPosition", + overlayElement: AutofillOverlayElement.List, + }, + sender, + ); + await flushPromises(); + jest.advanceTimersByTime(150); + + expect( + overlayBackground["updateInlineMenuPositionAfterRepositionEvent"], + ).toHaveBeenCalled(); + }); + }); + + describe("updateAutofillInlineMenuElementIsVisibleStatus message handler", () => { + let sender: chrome.runtime.MessageSender; + let focusedFieldData: FocusedFieldData; + + beforeEach(() => { + sender = mock({ tab: { id: 1 } }); + focusedFieldData = createFocusedFieldDataMock(); + overlayBackground["isInlineMenuButtonVisible"] = true; + overlayBackground["isInlineMenuListVisible"] = false; + }); + + it("skips updating the inline menu visibility status if the sender tab does not contain the focused field", async () => { + const otherSender = mock({ tab: { id: 2 } }); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData }, + otherSender, + ); + + sendMockExtensionMessage( + { + command: "updateAutofillInlineMenuElementIsVisibleStatus", + overlayElement: AutofillOverlayElement.Button, + isVisible: false, + }, + sender, + ); + + expect(overlayBackground["isInlineMenuButtonVisible"]).toBe(true); + expect(overlayBackground["isInlineMenuListVisible"]).toBe(false); + }); + + it("updates the visibility status of the inline menu button", async () => { + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + + sendMockExtensionMessage( + { + command: "updateAutofillInlineMenuElementIsVisibleStatus", + overlayElement: AutofillOverlayElement.Button, + isVisible: false, + }, + sender, + ); + + expect(overlayBackground["isInlineMenuButtonVisible"]).toBe(false); + expect(overlayBackground["isInlineMenuListVisible"]).toBe(false); + }); + + it("updates the visibility status of the inline menu list", async () => { + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + + sendMockExtensionMessage( + { + command: "updateAutofillInlineMenuElementIsVisibleStatus", + overlayElement: AutofillOverlayElement.List, + isVisible: true, + }, + sender, + ); + + expect(overlayBackground["isInlineMenuButtonVisible"]).toBe(true); + expect(overlayBackground["isInlineMenuListVisible"]).toBe(true); + }); + }); + + describe("checkIsAutofillInlineMenuButtonVisible message handler", () => { + it("returns true when the inline menu button is visible", async () => { + overlayBackground["isInlineMenuButtonVisible"] = true; + const sender = mock({ tab: { id: 1 } }); + + sendMockExtensionMessage( + { command: "checkIsAutofillInlineMenuButtonVisible" }, + sender, + sendResponse, + ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(true); + }); + }); + + describe("checkIsAutofillInlineMenuListVisible message handler", () => { + it("returns true when the inline menu list is visible", async () => { + overlayBackground["isInlineMenuListVisible"] = true; + const sender = mock({ tab: { id: 1 } }); + + sendMockExtensionMessage( + { command: "checkIsAutofillInlineMenuListVisible" }, + sender, + sendResponse, + ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(true); + }); + }); + + describe("getCurrentTabFrameId message handler", () => { + it("returns the sender's frame id", async () => { + const sender = mock({ frameId: 1 }); + + sendMockExtensionMessage({ command: "getCurrentTabFrameId" }, sender, sendResponse); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(1); + }); + }); + + describe("destroyAutofillInlineMenuListeners", () => { + it("sends a message to the passed frameId that triggers a destruction of the inline menu listeners on that frame", () => { + const sender = mock({ tab: { id: 1 }, frameId: 0 }); + + sendMockExtensionMessage( + { command: "destroyAutofillInlineMenuListeners", subFrameData: { frameId: 10 } }, + sender, + ); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { command: "destroyAutofillInlineMenuListeners" }, + { frameId: 10 }, + ); + }); + }); + + describe("unlockCompleted", () => { + let updateInlineMenuCiphersSpy: jest.SpyInstance; + + beforeEach(async () => { + updateInlineMenuCiphersSpy = jest.spyOn(overlayBackground, "updateOverlayCiphers"); + await initOverlayElementPorts(); + }); + + it("updates the inline menu button auth status", async () => { + sendMockExtensionMessage({ command: "unlockCompleted" }); + await flushPromises(); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateInlineMenuButtonAuthStatus", + authStatus: AuthenticationStatus.Unlocked, + }); + }); + + it("updates the overlay ciphers", async () => { + const updateInlineMenuCiphersSpy = jest.spyOn(overlayBackground, "updateOverlayCiphers"); + sendMockExtensionMessage({ command: "unlockCompleted" }); + await flushPromises(); + + expect(updateInlineMenuCiphersSpy).toHaveBeenCalled(); + }); + + it("opens the inline menu if a retry command is present in the message", async () => { + updateInlineMenuCiphersSpy.mockImplementation(); + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(createChromeTabMock({ id: 1 })); + sendMockExtensionMessage({ + command: "unlockCompleted", + data: { + commandToRetry: { message: { command: "openAutofillInlineMenu" } }, + }, + }); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + expect.any(Object), + { + command: "openAutofillInlineMenu", + isFocusingFieldElement: true, + isOpeningFullInlineMenu: false, + authStatus: AuthenticationStatus.Unlocked, + }, + { frameId: 0 }, + ); + }); + }); + + describe("extension messages that trigger an update of the inline menu ciphers", () => { + const extensionMessages = [ + "doFullSync", + "addedCipher", + "addEditCipherSubmitted", + "editedCipher", + "deletedCipher", ]; - translationKeys.forEach((key) => { - expect(overlayBackground["i18nService"].translate).toHaveBeenCalledWith(key); + + beforeEach(() => { + jest.spyOn(overlayBackground, "updateOverlayCiphers").mockImplementation(); }); - expect(translations).toStrictEqual({ - locale: "en", - opensInANewWindow: "opensInANewWindow", - buttonPageTitle: "bitwardenOverlayButton", - toggleBitwardenVaultOverlay: "toggleBitwardenVaultOverlay", - listPageTitle: "bitwardenVault", - unlockYourAccount: "unlockYourAccountToViewMatchingLogins", - unlockAccount: "unlockAccount", - fillCredentialsFor: "fillCredentialsFor", - partialUsername: "partialUsername", - view: "view", - noItemsToShow: "noItemsToShow", - newItem: "newItem", - addNewVaultItem: "addNewVaultItem", + + extensionMessages.forEach((message) => { + it(`triggers an update of the overlay ciphers when the ${message} message is received`, () => { + sendMockExtensionMessage({ command: message }); + expect(overlayBackground.updateOverlayCiphers).toHaveBeenCalled(); + }); }); }); }); - describe("setupExtensionMessageListeners", () => { - it("will set up onMessage and onConnect listeners", () => { - overlayBackground["setupExtensionMessageListeners"](); - - // eslint-disable-next-line - expect(chrome.runtime.onMessage.addListener).toHaveBeenCalled(); - expect(chrome.runtime.onConnect.addListener).toHaveBeenCalled(); - }); - }); - - describe("handleExtensionMessage", () => { + describe("handle extension onMessage", () => { it("will return early if the message command is not present within the extensionMessageHandlers", () => { const message = { command: "not-a-command", @@ -494,970 +1642,591 @@ describe("OverlayBackground", () => { sendResponse, ); - expect(returnValue).toBe(undefined); + expect(returnValue).toBe(null); expect(sendResponse).not.toHaveBeenCalled(); }); + }); - it("will trigger the message handler and return undefined if the message does not have a response", () => { - const message = { - command: "autofillOverlayElementClosed", - }; - const sender = mock({ tab: { id: 1 } }); - const sendResponse = jest.fn(); - jest.spyOn(overlayBackground as any, "overlayElementClosed"); + describe("inline menu button message handlers", () => { + let sender: chrome.runtime.MessageSender; + const portKey = "inlineMenuButtonPort"; - const returnValue = overlayBackground["handleExtensionMessage"]( - message, - sender, - sendResponse, - ); - - expect(returnValue).toBe(undefined); - expect(sendResponse).not.toHaveBeenCalled(); - expect(overlayBackground["overlayElementClosed"]).toHaveBeenCalledWith(message, sender); + beforeEach(async () => { + sender = mock({ tab: { id: 1 } }); + portKeyForTabSpy[sender.tab.id] = portKey; + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + await initOverlayElementPorts(); + buttonMessageConnectorSpy.sender = sender; + openUnlockPopoutSpy.mockImplementation(); }); - it("will return a response if the message handler returns a response", async () => { - const message = { - command: "openAutofillOverlay", - }; - const sender = mock({ tab: { id: 1 } }); - const sendResponse = jest.fn(); - jest.spyOn(overlayBackground as any, "getTranslations").mockReturnValue("translations"); + describe("autofillInlineMenuButtonClicked message handler", () => { + it("opens the unlock vault popout if the user auth status is not unlocked", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Locked); + tabsSendMessageSpy.mockImplementation(); - const returnValue = overlayBackground["handleExtensionMessage"]( - message, - sender, - sendResponse, - ); + sendPortMessage(buttonMessageConnectorSpy, { + command: "autofillInlineMenuButtonClicked", + portKey, + }); + await flushPromises(); - expect(returnValue).toBe(true); + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { command: "closeAutofillInlineMenu", overlayElement: undefined }, + { frameId: 0 }, + ); + expect(tabSendMessageDataSpy).toBeCalledWith( + sender.tab, + "addToLockedVaultPendingNotifications", + { + commandToRetry: { message: { command: "openAutofillInlineMenu" }, sender }, + target: "overlay.background", + }, + ); + expect(openUnlockPopoutSpy).toHaveBeenCalled(); + }); + + it("opens the inline menu if the user auth status is unlocked", async () => { + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(sender.tab); + sendPortMessage(buttonMessageConnectorSpy, { + command: "autofillInlineMenuButtonClicked", + portKey, + }); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "openAutofillInlineMenu", + isFocusingFieldElement: false, + isOpeningFullInlineMenu: true, + authStatus: AuthenticationStatus.Unlocked, + }, + { frameId: 0 }, + ); + }); }); - describe("extension message handlers", () => { - beforeEach(() => { - jest - .spyOn(overlayBackground as any, "getAuthStatus") - .mockResolvedValue(AuthenticationStatus.Unlocked); + describe("triggerDelayedAutofillInlineMenuClosure message handler", () => { + it("skips triggering the delayed closure of the inline menu if a field is currently focused", async () => { + jest.useFakeTimers(); + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFocused", + isFieldCurrentlyFocused: true, + }); + await flushPromises(); + + sendPortMessage(buttonMessageConnectorSpy, { + command: "triggerDelayedAutofillInlineMenuClosure", + portKey, + }); + await flushPromises(); + jest.advanceTimersByTime(100); + + const message = { command: "triggerDelayedAutofillInlineMenuClosure" }; + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith(message); + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith(message); }); - describe("openAutofillOverlay message handler", () => { - it("opens the autofill overlay by sending a message to the current tab", async () => { - const sender = mock({ tab: { id: 1 } }); - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(sender.tab); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); - - sendMockExtensionMessage({ command: "openAutofillOverlay" }); - await flushPromises(); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - sender.tab, - "openAutofillOverlay", - { - isFocusingFieldElement: false, - isOpeningFullOverlay: false, - authStatus: AuthenticationStatus.Unlocked, - }, - ); + it("sends a message to the button and list ports that triggers a delayed closure of the inline menu", async () => { + jest.useFakeTimers(); + sendPortMessage(buttonMessageConnectorSpy, { + command: "triggerDelayedAutofillInlineMenuClosure", + portKey, }); + await flushPromises(); + jest.advanceTimersByTime(100); + + const message = { command: "triggerDelayedAutofillInlineMenuClosure" }; + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith(message); + expect(listPortSpy.postMessage).toHaveBeenCalledWith(message); }); - describe("autofillOverlayElementClosed message handler", () => { - beforeEach(async () => { - await initOverlayElementPorts(); + it("triggers a single delayed closure if called again within a 100ms threshold", async () => { + jest.useFakeTimers(); + sendPortMessage(buttonMessageConnectorSpy, { + command: "triggerDelayedAutofillInlineMenuClosure", + portKey, }); - - it("disconnects any expired ports if the sender is not from the same page as the most recently focused field", () => { - const port1 = mock(); - const port2 = mock(); - overlayBackground["expiredPorts"] = [port1, port2]; - const sender = mock({ tab: { id: 1 } }); - const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage( - { - command: "autofillOverlayElementClosed", - overlayElement: AutofillOverlayElement.Button, - }, - sender, - ); - - expect(port1.disconnect).toHaveBeenCalled(); - expect(port2.disconnect).toHaveBeenCalled(); + await flushPromises(); + jest.advanceTimersByTime(50); + sendPortMessage(buttonMessageConnectorSpy, { + command: "triggerDelayedAutofillInlineMenuClosure", + portKey, }); + await flushPromises(); + jest.advanceTimersByTime(100); - it("disconnects the button element port", () => { - sendMockExtensionMessage({ - command: "autofillOverlayElementClosed", - overlayElement: AutofillOverlayElement.Button, - }); + const message = { command: "triggerDelayedAutofillInlineMenuClosure" }; + expect(buttonPortSpy.postMessage).toHaveBeenCalledTimes(2); + expect(buttonPortSpy.postMessage).not.toHaveBeenNthCalledWith(1, message); + expect(buttonPortSpy.postMessage).toHaveBeenNthCalledWith(2, message); + expect(listPortSpy.postMessage).toHaveBeenCalledTimes(2); + expect(listPortSpy.postMessage).not.toHaveBeenNthCalledWith(1, message); + expect(listPortSpy.postMessage).toHaveBeenNthCalledWith(2, message); + }); + }); - expect(buttonPortSpy.disconnect).toHaveBeenCalled(); - expect(overlayBackground["overlayButtonPort"]).toBeNull(); + describe("autofillInlineMenuBlurred message handler", () => { + it("sends a message to the inline menu list to check if the element is focused", async () => { + sendPortMessage(buttonMessageConnectorSpy, { + command: "autofillInlineMenuBlurred", + portKey, }); + await flushPromises(); - it("disconnects the list element port", () => { - sendMockExtensionMessage({ - command: "autofillOverlayElementClosed", - overlayElement: AutofillOverlayElement.List, - }); - - expect(listPortSpy.disconnect).toHaveBeenCalled(); - expect(overlayBackground["overlayListPort"]).toBeNull(); + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuListFocused", }); }); + }); - describe("autofillOverlayAddNewVaultItem message handler", () => { - let sender: chrome.runtime.MessageSender; - beforeEach(() => { - sender = mock({ tab: { id: 1 } }); - jest - .spyOn(overlayBackground["cipherService"], "setAddEditCipherInfo") - .mockImplementation(); - jest.spyOn(overlayBackground as any, "openAddEditVaultItemPopout").mockImplementation(); + describe("redirectAutofillInlineMenuFocusOut message handler", () => { + it("ignores the redirect message if the direction is not provided", () => { + sendPortMessage(buttonMessageConnectorSpy, { + command: "redirectAutofillInlineMenuFocusOut", + portKey, }); - it("will not open the add edit popout window if the message does not have a login cipher provided", () => { - sendMockExtensionMessage({ command: "autofillOverlayAddNewVaultItem" }, sender); - - expect(overlayBackground["cipherService"].setAddEditCipherInfo).not.toHaveBeenCalled(); - expect(overlayBackground["openAddEditVaultItemPopout"]).not.toHaveBeenCalled(); - }); - - it("will open the add edit popout window after creating a new cipher", async () => { - jest.spyOn(BrowserApi, "sendMessage"); - - sendMockExtensionMessage( - { - command: "autofillOverlayAddNewVaultItem", - login: { - uri: "https://tacos.com", - hostname: "", - username: "username", - password: "password", - }, - }, - sender, - ); - await flushPromises(); - - expect(overlayBackground["cipherService"].setAddEditCipherInfo).toHaveBeenCalled(); - expect(BrowserApi.sendMessage).toHaveBeenCalledWith( - "inlineAutofillMenuRefreshAddEditCipher", - ); - expect(overlayBackground["openAddEditVaultItemPopout"]).toHaveBeenCalled(); - }); + expect(tabSendMessageDataSpy).not.toHaveBeenCalled(); }); - describe("getAutofillOverlayVisibility message handler", () => { - beforeEach(() => { - jest - .spyOn(overlayBackground as any, "getOverlayVisibility") - .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); + it("sends the redirect message if the direction is provided", () => { + sendPortMessage(buttonMessageConnectorSpy, { + command: "redirectAutofillInlineMenuFocusOut", + direction: RedirectFocusDirection.Next, + portKey, }); - it("will set the overlayVisibility property", async () => { - sendMockExtensionMessage({ command: "getAutofillOverlayVisibility" }); - await flushPromises(); - - expect(await overlayBackground["getOverlayVisibility"]()).toBe( - AutofillOverlayVisibility.OnFieldFocus, - ); - }); - - it("returns the overlayVisibility property", async () => { - const sendMessageSpy = jest.fn(); - - sendMockExtensionMessage( - { command: "getAutofillOverlayVisibility" }, - undefined, - sendMessageSpy, - ); - await flushPromises(); - - expect(sendMessageSpy).toHaveBeenCalledWith(AutofillOverlayVisibility.OnFieldFocus); - }); + expect(tabSendMessageDataSpy).toHaveBeenCalledWith( + sender.tab, + "redirectAutofillInlineMenuFocusOut", + { direction: RedirectFocusDirection.Next }, + ); }); + }); - describe("checkAutofillOverlayFocused message handler", () => { - beforeEach(async () => { - await initOverlayElementPorts(); + describe("updateAutofillInlineMenuColorScheme message handler", () => { + it("sends a message to the button port to update the inline menu color scheme", async () => { + sendPortMessage(buttonMessageConnectorSpy, { + command: "updateAutofillInlineMenuColorScheme", + portKey, }); + await flushPromises(); - it("will check if the overlay list is focused if the list port is open", () => { - sendMockExtensionMessage({ command: "checkAutofillOverlayFocused" }); - - expect(listPortSpy.postMessage).toHaveBeenCalledWith({ - command: "checkAutofillOverlayListFocused", - }); - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "checkAutofillOverlayButtonFocused", - }); - }); - - it("will check if the overlay button is focused if the list port is not open", () => { - overlayBackground["overlayListPort"] = undefined; - - sendMockExtensionMessage({ command: "checkAutofillOverlayFocused" }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "checkAutofillOverlayButtonFocused", - }); - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "checkAutofillOverlayListFocused", - }); - }); - }); - - describe("focusAutofillOverlayList message handler", () => { - it("will send a `focusOverlayList` message to the overlay list port", async () => { - await initOverlayElementPorts({ initList: true, initButton: false }); - - sendMockExtensionMessage({ command: "focusAutofillOverlayList" }); - - expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "focusOverlayList" }); - }); - }); - - describe("updateAutofillOverlayPosition message handler", () => { - beforeEach(async () => { - await overlayBackground["handlePortOnConnect"]( - createPortSpyMock(AutofillOverlayPort.List), - ); - listPortSpy = overlayBackground["overlayListPort"]; - - await overlayBackground["handlePortOnConnect"]( - createPortSpyMock(AutofillOverlayPort.Button), - ); - buttonPortSpy = overlayBackground["overlayButtonPort"]; - }); - - it("ignores updating the position if the overlay element type is not provided", () => { - sendMockExtensionMessage({ command: "updateAutofillOverlayPosition" }); - - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - }); - - it("skips updating the position if the most recently focused field is different than the message sender", () => { - const sender = mock({ tab: { id: 1 } }); - const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ command: "updateAutofillOverlayPosition" }, sender); - - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - }); - - it("updates the overlay button's position", () => { - const focusedFieldData = createFocusedFieldDataMock(); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "2px", left: "4px", top: "2px", width: "2px" }, - }); - }); - - it("modifies the overlay button's height for medium sized input elements", () => { - const focusedFieldData = createFocusedFieldDataMock({ - focusedFieldRects: { top: 1, left: 2, height: 35, width: 4 }, - }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "20px", left: "-22px", top: "8px", width: "20px" }, - }); - }); - - it("modifies the overlay button's height for large sized input elements", () => { - const focusedFieldData = createFocusedFieldDataMock({ - focusedFieldRects: { top: 1, left: 2, height: 50, width: 4 }, - }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "27px", left: "-32px", top: "13px", width: "27px" }, - }); - }); - - it("takes into account the right padding of the focused field in positioning the button if the right padding of the field is larger than the left padding", () => { - const focusedFieldData = createFocusedFieldDataMock({ - focusedFieldStyles: { paddingRight: "20px", paddingLeft: "6px" }, - }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "2px", left: "-18px", top: "2px", width: "2px" }, - }); - }); - - it("will post a message to the overlay list facilitating an update of the list's position", () => { - const sender = mock({ tab: { id: 1 } }); - const focusedFieldData = createFocusedFieldDataMock(); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - overlayBackground["updateOverlayPosition"]( - { overlayElement: AutofillOverlayElement.List }, - sender, - ); - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.List, - }); - - expect(listPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { left: "2px", top: "4px", width: "4px" }, - }); - }); - }); - - describe("updateOverlayHidden", () => { - beforeEach(async () => { - await initOverlayElementPorts(); - }); - - it("returns early if the display value is not provided", () => { - const message = { - command: "updateAutofillOverlayHidden", - }; - - sendMockExtensionMessage(message); - - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith(message); - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith(message); - }); - - it("posts a message to the overlay button and list with the display value", () => { - const message = { command: "updateAutofillOverlayHidden", display: "none" }; - - sendMockExtensionMessage(message); - - expect(overlayBackground["overlayButtonPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayHidden", - styles: { - display: message.display, - }, - }); - expect(overlayBackground["overlayListPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayHidden", - styles: { - display: message.display, - }, - }); - }); - }); - - describe("collectPageDetailsResponse message handler", () => { - let sender: chrome.runtime.MessageSender; - const pageDetails1 = createAutofillPageDetailsMock({ - login: { username: "username1", password: "password1" }, - }); - const pageDetails2 = createAutofillPageDetailsMock({ - login: { username: "username2", password: "password2" }, - }); - - beforeEach(() => { - sender = mock({ tab: { id: 1 } }); - }); - - it("stores the page details provided by the message by the tab id of the sender", () => { - sendMockExtensionMessage( - { command: "collectPageDetailsResponse", details: pageDetails1 }, - sender, - ); - - expect(overlayBackground["pageDetailsForTab"][sender.tab.id]).toStrictEqual( - new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], - ]), - ); - }); - - it("updates the page details for a tab that already has a set of page details stored ", () => { - const secondFrameSender = mock({ - tab: { id: 1 }, - frameId: 3, - }); - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], - ]); - - sendMockExtensionMessage( - { command: "collectPageDetailsResponse", details: pageDetails2 }, - secondFrameSender, - ); - - expect(overlayBackground["pageDetailsForTab"][sender.tab.id]).toStrictEqual( - new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], - [ - secondFrameSender.frameId, - { - frameId: secondFrameSender.frameId, - tab: secondFrameSender.tab, - details: pageDetails2, - }, - ], - ]), - ); - }); - }); - - describe("unlockCompleted message handler", () => { - let getAuthStatusSpy: jest.SpyInstance; - - beforeEach(() => { - overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; - jest.spyOn(BrowserApi, "tabSendMessageData"); - getAuthStatusSpy = jest - .spyOn(overlayBackground as any, "getAuthStatus") - .mockImplementation(() => { - overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; - return Promise.resolve(AuthenticationStatus.Unlocked); - }); - }); - - it("updates the user's auth status but does not open the overlay", async () => { - const message = { - command: "unlockCompleted", - data: { - commandToRetry: { message: { command: "" } }, - }, - }; - - sendMockExtensionMessage(message); - await flushPromises(); - - expect(getAuthStatusSpy).toHaveBeenCalled(); - expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled(); - }); - - it("updates user's auth status and opens the overlay if a follow up command is provided", async () => { - const sender = mock({ tab: { id: 1 } }); - const message = { - command: "unlockCompleted", - data: { - commandToRetry: { message: { command: "openAutofillOverlay" } }, - }, - }; - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(sender.tab); - - sendMockExtensionMessage(message); - await flushPromises(); - - expect(getAuthStatusSpy).toHaveBeenCalled(); - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - sender.tab, - "openAutofillOverlay", - { - isFocusingFieldElement: true, - isOpeningFullOverlay: false, - authStatus: AuthenticationStatus.Unlocked, - }, - ); - }); - }); - - describe("extension messages that trigger an update of the inline menu ciphers", () => { - const extensionMessages = [ - "addedCipher", - "addEditCipherSubmitted", - "editedCipher", - "deletedCipher", - ]; - - beforeEach(() => { - jest.spyOn(overlayBackground, "updateOverlayCiphers").mockImplementation(); - }); - - extensionMessages.forEach((message) => { - it(`triggers an update of the overlay ciphers when the ${message} message is received`, () => { - sendMockExtensionMessage({ command: message }); - expect(overlayBackground.updateOverlayCiphers).toHaveBeenCalled(); - }); + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuColorScheme", }); }); }); }); - describe("handlePortOnConnect", () => { - beforeEach(() => { - jest.spyOn(overlayBackground as any, "updateOverlayPosition").mockImplementation(); - jest.spyOn(overlayBackground as any, "getAuthStatus").mockImplementation(); - jest.spyOn(overlayBackground as any, "getTranslations").mockImplementation(); - jest.spyOn(overlayBackground as any, "getOverlayCipherData").mockImplementation(); + describe("inline menu list message handlers", () => { + let sender: chrome.runtime.MessageSender; + const portKey = "inlineMenuListPort"; + + beforeEach(async () => { + sender = mock({ tab: { id: 1 } }); + portKeyForTabSpy[sender.tab.id] = portKey; + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + await initOverlayElementPorts(); + listMessageConnectorSpy.sender = sender; + openUnlockPopoutSpy.mockImplementation(); }); + describe("checkAutofillInlineMenuButtonFocused message handler", () => { + it("sends a message to the inline menu button to check if the element is focused", async () => { + sendPortMessage(listMessageConnectorSpy, { + command: "checkAutofillInlineMenuButtonFocused", + portKey, + }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuButtonFocused", + }); + }); + }); + + describe("autofillInlineMenuBlurred message handler", () => { + it("sends a message to the inline menu button to check if the element is focused", async () => { + sendPortMessage(listMessageConnectorSpy, { + command: "autofillInlineMenuBlurred", + portKey, + }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuButtonFocused", + }); + }); + }); + + describe("unlockVault message handler", () => { + it("opens the unlock vault popout", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Locked); + tabsSendMessageSpy.mockImplementation(); + + sendPortMessage(listMessageConnectorSpy, { command: "unlockVault", portKey }); + await flushPromises(); + + expect(openUnlockPopoutSpy).toHaveBeenCalled(); + }); + }); + + describe("fillAutofillInlineMenuCipher message handler", () => { + const pageDetails = createAutofillPageDetailsMock({ + login: { username: "username1", password: "password1" }, + }); + + it("ignores the fill request if the overlay cipher id is not provided", async () => { + sendPortMessage(listMessageConnectorSpy, { + command: "fillAutofillInlineMenuCipher", + portKey, + }); + await flushPromises(); + + expect(autofillService.isPasswordRepromptRequired).not.toHaveBeenCalled(); + expect(autofillService.doAutoFill).not.toHaveBeenCalled(); + }); + + it("ignores the fill request if the tab does not contain any identified page details", async () => { + sendPortMessage(listMessageConnectorSpy, { + command: "fillAutofillInlineMenuCipher", + inlineMenuCipherId: "inline-menu-cipher-1", + portKey, + }); + await flushPromises(); + + expect(autofillService.isPasswordRepromptRequired).not.toHaveBeenCalled(); + expect(autofillService.doAutoFill).not.toHaveBeenCalled(); + }); + + it("ignores the fill request if a master password reprompt is required", async () => { + const cipher = mock({ + reprompt: CipherRepromptType.Password, + type: CipherType.Login, + }); + overlayBackground["inlineMenuCiphers"] = new Map([["inline-menu-cipher-1", cipher]]); + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], + ]); + autofillService.isPasswordRepromptRequired.mockResolvedValue(true); + + sendPortMessage(listMessageConnectorSpy, { + command: "fillAutofillInlineMenuCipher", + inlineMenuCipherId: "inline-menu-cipher-1", + portKey, + }); + await flushPromises(); + + expect(autofillService.isPasswordRepromptRequired).toHaveBeenCalledWith(cipher, sender.tab); + expect(autofillService.doAutoFill).not.toHaveBeenCalled(); + }); + + it("auto-fills the selected cipher and move it to the top of the front of the ciphers map", async () => { + const cipher1 = mock({ id: "inline-menu-cipher-1" }); + const cipher2 = mock({ id: "inline-menu-cipher-2" }); + const cipher3 = mock({ id: "inline-menu-cipher-3" }); + overlayBackground["inlineMenuCiphers"] = new Map([ + ["inline-menu-cipher-1", cipher1], + ["inline-menu-cipher-2", cipher2], + ["inline-menu-cipher-3", cipher3], + ]); + const pageDetailsForTab = { + frameId: sender.frameId, + tab: sender.tab, + details: pageDetails, + }; + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, pageDetailsForTab], + ]); + autofillService.isPasswordRepromptRequired.mockResolvedValue(false); + + sendPortMessage(listMessageConnectorSpy, { + command: "fillAutofillInlineMenuCipher", + inlineMenuCipherId: "inline-menu-cipher-2", + portKey, + }); + await flushPromises(); + + expect(autofillService.isPasswordRepromptRequired).toHaveBeenCalledWith( + cipher2, + sender.tab, + ); + expect(autofillService.doAutoFill).toHaveBeenCalledWith({ + tab: sender.tab, + cipher: cipher2, + pageDetails: [pageDetailsForTab], + fillNewPassword: true, + allowTotpAutofill: true, + }); + expect(overlayBackground["inlineMenuCiphers"].entries()).toStrictEqual( + new Map([ + ["inline-menu-cipher-2", cipher2], + ["inline-menu-cipher-1", cipher1], + ["inline-menu-cipher-3", cipher3], + ]).entries(), + ); + }); + + it("copies the cipher's totp code to the clipboard after filling", async () => { + const cipher1 = mock({ id: "inline-menu-cipher-1" }); + overlayBackground["inlineMenuCiphers"] = new Map([["inline-menu-cipher-1", cipher1]]); + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], + ]); + autofillService.isPasswordRepromptRequired.mockResolvedValue(false); + const copyToClipboardSpy = jest + .spyOn(overlayBackground["platformUtilsService"], "copyToClipboard") + .mockImplementation(); + autofillService.doAutoFill.mockResolvedValue("totp-code"); + + sendPortMessage(listMessageConnectorSpy, { + command: "fillAutofillInlineMenuCipher", + inlineMenuCipherId: "inline-menu-cipher-2", + portKey, + }); + await flushPromises(); + + expect(copyToClipboardSpy).toHaveBeenCalledWith("totp-code"); + }); + }); + + describe("addNewVaultItem message handler", () => { + it("skips sending the `addNewVaultItemFromOverlay` message if the sender tab does not contain the focused field", async () => { + const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + await flushPromises(); + + sendPortMessage(listMessageConnectorSpy, { command: "addNewVaultItem", portKey }); + await flushPromises(); + + expect(tabsSendMessageSpy).not.toHaveBeenCalled(); + }); + + it("sends a message to the tab to add a new vault item", async () => { + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + await flushPromises(); + + sendPortMessage(listMessageConnectorSpy, { command: "addNewVaultItem", portKey }); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { command: "addNewVaultItemFromOverlay" }, + { frameId: sender.frameId }, + ); + }); + }); + + describe("viewSelectedCipher message handler", () => { + let openViewVaultItemPopoutSpy: jest.SpyInstance; + + beforeEach(() => { + openViewVaultItemPopoutSpy = jest + .spyOn(overlayBackground as any, "openViewVaultItemPopout") + .mockImplementation(); + }); + + it("returns early if the passed cipher ID does not match one of the inline menu ciphers", async () => { + overlayBackground["inlineMenuCiphers"] = new Map([ + ["inline-menu-cipher-0", mock({ id: "inline-menu-cipher-0" })], + ]); + + sendPortMessage(listMessageConnectorSpy, { + command: "viewSelectedCipher", + inlineMenuCipherId: "inline-menu-cipher-1", + portKey, + }); + await flushPromises(); + + expect(openViewVaultItemPopoutSpy).not.toHaveBeenCalled(); + }); + + it("will open the view vault item popout with the selected cipher", async () => { + const cipher = mock({ id: "inline-menu-cipher-1" }); + overlayBackground["inlineMenuCiphers"] = new Map([ + ["inline-menu-cipher-0", mock({ id: "inline-menu-cipher-0" })], + ["inline-menu-cipher-1", cipher], + ]); + + sendPortMessage(listMessageConnectorSpy, { + command: "viewSelectedCipher", + inlineMenuCipherId: "inline-menu-cipher-1", + portKey, + }); + await flushPromises(); + + expect(openViewVaultItemPopoutSpy).toHaveBeenCalledWith(sender.tab, { + cipherId: cipher.id, + action: SHOW_AUTOFILL_BUTTON, + }); + }); + }); + + describe("redirectAutofillInlineMenuFocusOut message handler", () => { + it("redirects focus out of the inline menu list", async () => { + sendPortMessage(listMessageConnectorSpy, { + command: "redirectAutofillInlineMenuFocusOut", + direction: RedirectFocusDirection.Next, + portKey, + }); + await flushPromises(); + + expect(tabSendMessageDataSpy).toHaveBeenCalledWith( + sender.tab, + "redirectAutofillInlineMenuFocusOut", + { direction: RedirectFocusDirection.Next }, + ); + }); + }); + + describe("updateAutofillInlineMenuListHeight message handler", () => { + it("sends a message to the list port to update the menu iframe position", () => { + sendPortMessage(listMessageConnectorSpy, { + command: "updateAutofillInlineMenuListHeight", + styles: { height: "100px" }, + portKey, + }); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuPosition", + styles: { height: "100px" }, + }); + }); + }); + }); + + describe("handle web navigation on committed events", () => { + describe("navigation event occurs in the top frame of the tab", () => { + it("removes the collected page details", async () => { + const sender = mock({ + tabId: 1, + frameId: 0, + }); + overlayBackground["pageDetailsForTab"][sender.tabId] = new Map([ + [sender.frameId, createPageDetailMock()], + ]); + + triggerWebNavigationOnCommittedEvent(sender); + await flushPromises(); + + expect(overlayBackground["pageDetailsForTab"][sender.tabId]).toBe(undefined); + }); + + it("clears the sub frames associated with the tab", () => { + const sender = mock({ + tabId: 1, + frameId: 0, + }); + const subFrameId = 10; + overlayBackground["subFrameOffsetsForTab"][sender.tabId] = new Map([ + [subFrameId, mock()], + ]); + + triggerWebNavigationOnCommittedEvent(sender); + + expect(overlayBackground["subFrameOffsetsForTab"][sender.tabId]).toBe(undefined); + }); + }); + + describe("navigation event occurs within sub frame", () => { + it("clears the sub frame offsets for the current frame", () => { + const sender = mock({ + tabId: 1, + frameId: 1, + }); + overlayBackground["subFrameOffsetsForTab"][sender.tabId] = new Map([ + [sender.frameId, mock()], + ]); + + triggerWebNavigationOnCommittedEvent(sender); + + expect(overlayBackground["subFrameOffsetsForTab"][sender.tabId].get(sender.frameId)).toBe( + undefined, + ); + }); + }); + }); + + describe("handle port onConnect", () => { it("skips setting up the overlay port if the port connection is not for an overlay element", async () => { const port = createPortSpyMock("not-an-overlay-element"); - await overlayBackground["handlePortOnConnect"](port); + triggerPortOnConnectEvent(port); + await flushPromises(); expect(port.onMessage.addListener).not.toHaveBeenCalled(); expect(port.postMessage).not.toHaveBeenCalled(); }); - it("sets up the overlay list port if the port connection is for the overlay list", async () => { - await initOverlayElementPorts({ initList: true, initButton: false }); + it("generates a random 12 character string used to validate port messages from the tab", async () => { + const port = createPortSpyMock(AutofillOverlayPort.Button); + overlayBackground["inlineMenuButtonPort"] = port; + + triggerPortOnConnectEvent(port); await flushPromises(); - expect(overlayBackground["overlayButtonPort"]).toBeUndefined(); - expect(listPortSpy.onMessage.addListener).toHaveBeenCalled(); - expect(listPortSpy.postMessage).toHaveBeenCalled(); - expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); - expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/list.css"); - expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); - expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith( - { overlayElement: AutofillOverlayElement.List }, - listPortSpy.sender, - ); - }); - - it("sets up the overlay button port if the port connection is for the overlay button", async () => { - await initOverlayElementPorts({ initList: false, initButton: true }); - await flushPromises(); - - expect(overlayBackground["overlayListPort"]).toBeUndefined(); - expect(buttonPortSpy.onMessage.addListener).toHaveBeenCalled(); - expect(buttonPortSpy.postMessage).toHaveBeenCalled(); - expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); - expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/button.css"); - expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith( - { overlayElement: AutofillOverlayElement.Button }, - buttonPortSpy.sender, - ); + expect(portKeyForTabSpy[port.sender.tab.id]).toHaveLength(12); }); it("stores an existing overlay port so that it can be disconnected at a later time", async () => { - overlayBackground["overlayButtonPort"] = mock(); + overlayBackground["inlineMenuButtonPort"] = mock(); await initOverlayElementPorts({ initList: false, initButton: true }); await flushPromises(); expect(overlayBackground["expiredPorts"].length).toBe(1); }); + }); - it("gets the system theme", async () => { - themeStateService.selectedTheme$ = of(ThemeType.System); + describe("handle overlay element port onMessage", () => { + let sender: chrome.runtime.MessageSender; + const portKey = "inlineMenuListPort"; - await initOverlayElementPorts({ initList: true, initButton: false }); + beforeEach(async () => { + sender = mock({ tab: { id: 1 } }); + portKeyForTabSpy[sender.tab.id] = portKey; + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + await initOverlayElementPorts(); + listMessageConnectorSpy.sender = sender; + openUnlockPopoutSpy.mockImplementation(); + }); + + it("ignores messages that do not contain a valid portKey", async () => { + triggerPortOnMessageEvent(buttonMessageConnectorSpy, { + command: "autofillInlineMenuBlurred", + }); await flushPromises(); - expect(listPortSpy.postMessage).toHaveBeenCalledWith( - expect.objectContaining({ theme: ThemeType.System }), - ); + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuListFocused", + }); + }); + + it("ignores messages from ports that are not listened to", () => { + triggerPortOnMessageEvent(buttonPortSpy, { + command: "autofillInlineMenuBlurred", + portKey, + }); + + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuListFocused", + }); }); }); - describe("handleOverlayElementPortMessage", () => { - beforeEach(async () => { + describe("handle port onDisconnect", () => { + it("sets the disconnected port to a `null` value", async () => { await initOverlayElementPorts(); - overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; - }); - it("ignores port messages that do not contain a handler", () => { - jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); + triggerPortOnDisconnectEvent(buttonPortSpy); + triggerPortOnDisconnectEvent(listPortSpy); + await flushPromises(); - sendPortMessage(buttonPortSpy, { command: "checkAutofillOverlayButtonFocused" }); - - expect(overlayBackground["checkOverlayButtonFocused"]).not.toHaveBeenCalled(); - }); - - describe("overlay button message handlers", () => { - it("unlocks the vault if the user auth status is not unlocked", () => { - overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; - jest.spyOn(overlayBackground as any, "unlockVault").mockImplementation(); - - sendPortMessage(buttonPortSpy, { command: "overlayButtonClicked" }); - - expect(overlayBackground["unlockVault"]).toHaveBeenCalled(); - }); - - it("opens the autofill overlay if the auth status is unlocked", () => { - jest.spyOn(overlayBackground as any, "openOverlay").mockImplementation(); - - sendPortMessage(buttonPortSpy, { command: "overlayButtonClicked" }); - - expect(overlayBackground["openOverlay"]).toHaveBeenCalled(); - }); - - describe("closeAutofillOverlay", () => { - it("sends a `closeOverlay` message to the sender tab", () => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - - sendPortMessage(buttonPortSpy, { command: "closeAutofillOverlay" }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - buttonPortSpy.sender.tab, - "closeAutofillOverlay", - { forceCloseOverlay: false }, - ); - }); - }); - - describe("forceCloseAutofillOverlay", () => { - it("sends a `closeOverlay` message to the sender tab with a `forceCloseOverlay` flag of `true` set", () => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - - sendPortMessage(buttonPortSpy, { command: "forceCloseAutofillOverlay" }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - buttonPortSpy.sender.tab, - "closeAutofillOverlay", - { forceCloseOverlay: true }, - ); - }); - }); - - describe("overlayPageBlurred", () => { - it("checks if the overlay list is focused", () => { - jest.spyOn(overlayBackground as any, "checkOverlayListFocused"); - - sendPortMessage(buttonPortSpy, { command: "overlayPageBlurred" }); - - expect(overlayBackground["checkOverlayListFocused"]).toHaveBeenCalled(); - }); - }); - - describe("redirectOverlayFocusOut", () => { - beforeEach(() => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - }); - - it("ignores the redirect message if the direction is not provided", () => { - sendPortMessage(buttonPortSpy, { command: "redirectOverlayFocusOut" }); - - expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled(); - }); - - it("sends the redirect message if the direction is provided", () => { - sendPortMessage(buttonPortSpy, { - command: "redirectOverlayFocusOut", - direction: RedirectFocusDirection.Next, - }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - buttonPortSpy.sender.tab, - "redirectOverlayFocusOut", - { direction: RedirectFocusDirection.Next }, - ); - }); - }); - }); - - describe("overlay list message handlers", () => { - describe("checkAutofillOverlayButtonFocused", () => { - it("checks on the focus state of the overlay button", () => { - jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); - - sendPortMessage(listPortSpy, { command: "checkAutofillOverlayButtonFocused" }); - - expect(overlayBackground["checkOverlayButtonFocused"]).toHaveBeenCalled(); - }); - }); - - describe("forceCloseAutofillOverlay", () => { - it("sends a `closeOverlay` message to the sender tab with a `forceCloseOverlay` flag of `true` set", () => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - - sendPortMessage(listPortSpy, { command: "forceCloseAutofillOverlay" }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - listPortSpy.sender.tab, - "closeAutofillOverlay", - { forceCloseOverlay: true }, - ); - }); - }); - - describe("overlayPageBlurred", () => { - it("checks on the focus state of the overlay button", () => { - jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); - - sendPortMessage(listPortSpy, { command: "overlayPageBlurred" }); - - expect(overlayBackground["checkOverlayButtonFocused"]).toHaveBeenCalled(); - }); - }); - - describe("unlockVault", () => { - it("closes the autofill overlay and opens the unlock popout", async () => { - jest.spyOn(overlayBackground as any, "closeOverlay").mockImplementation(); - jest.spyOn(overlayBackground as any, "openUnlockPopout").mockImplementation(); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); - - sendPortMessage(listPortSpy, { command: "unlockVault" }); - await flushPromises(); - - expect(overlayBackground["closeOverlay"]).toHaveBeenCalledWith(listPortSpy); - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - listPortSpy.sender.tab, - "addToLockedVaultPendingNotifications", - { - commandToRetry: { - message: { command: "openAutofillOverlay" }, - sender: listPortSpy.sender, - }, - target: "overlay.background", - }, - ); - expect(overlayBackground["openUnlockPopout"]).toHaveBeenCalledWith( - listPortSpy.sender.tab, - true, - ); - }); - }); - - describe("fillSelectedListItem", () => { - let getLoginCiphersSpy: jest.SpyInstance; - let isPasswordRepromptRequiredSpy: jest.SpyInstance; - let doAutoFillSpy: jest.SpyInstance; - let sender: chrome.runtime.MessageSender; - const pageDetails = createAutofillPageDetailsMock({ - login: { username: "username1", password: "password1" }, - }); - - beforeEach(() => { - getLoginCiphersSpy = jest.spyOn(overlayBackground["overlayLoginCiphers"], "get"); - isPasswordRepromptRequiredSpy = jest.spyOn( - overlayBackground["autofillService"], - "isPasswordRepromptRequired", - ); - doAutoFillSpy = jest.spyOn(overlayBackground["autofillService"], "doAutoFill"); - sender = mock({ tab: { id: 1 } }); - }); - - it("ignores the fill request if the overlay cipher id is not provided", async () => { - sendPortMessage(listPortSpy, { command: "fillSelectedListItem" }); - await flushPromises(); - - expect(getLoginCiphersSpy).not.toHaveBeenCalled(); - expect(isPasswordRepromptRequiredSpy).not.toHaveBeenCalled(); - expect(doAutoFillSpy).not.toHaveBeenCalled(); - }); - - it("ignores the fill request if the tab does not contain any identified page details", async () => { - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(getLoginCiphersSpy).not.toHaveBeenCalled(); - expect(isPasswordRepromptRequiredSpy).not.toHaveBeenCalled(); - expect(doAutoFillSpy).not.toHaveBeenCalled(); - }); - - it("ignores the fill request if a master password reprompt is required", async () => { - const cipher = mock({ - reprompt: CipherRepromptType.Password, - type: CipherType.Login, - }); - overlayBackground["overlayLoginCiphers"] = new Map([["overlay-cipher-1", cipher]]); - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], - ]); - getLoginCiphersSpy = jest.spyOn(overlayBackground["overlayLoginCiphers"], "get"); - isPasswordRepromptRequiredSpy.mockResolvedValue(true); - - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(getLoginCiphersSpy).toHaveBeenCalled(); - expect(isPasswordRepromptRequiredSpy).toHaveBeenCalledWith( - cipher, - listPortSpy.sender.tab, - ); - expect(doAutoFillSpy).not.toHaveBeenCalled(); - }); - - it("auto-fills the selected cipher and move it to the top of the front of the ciphers map", async () => { - const cipher1 = mock({ id: "overlay-cipher-1" }); - const cipher2 = mock({ id: "overlay-cipher-2" }); - const cipher3 = mock({ id: "overlay-cipher-3" }); - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-1", cipher1], - ["overlay-cipher-2", cipher2], - ["overlay-cipher-3", cipher3], - ]); - const pageDetailsForTab = { - frameId: sender.frameId, - tab: sender.tab, - details: pageDetails, - }; - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, pageDetailsForTab], - ]); - isPasswordRepromptRequiredSpy.mockResolvedValue(false); - - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-2", - }); - await flushPromises(); - - expect(isPasswordRepromptRequiredSpy).toHaveBeenCalledWith( - cipher2, - listPortSpy.sender.tab, - ); - expect(doAutoFillSpy).toHaveBeenCalledWith({ - tab: listPortSpy.sender.tab, - cipher: cipher2, - pageDetails: [pageDetailsForTab], - fillNewPassword: true, - allowTotpAutofill: true, - }); - expect(overlayBackground["overlayLoginCiphers"].entries()).toStrictEqual( - new Map([ - ["overlay-cipher-2", cipher2], - ["overlay-cipher-1", cipher1], - ["overlay-cipher-3", cipher3], - ]).entries(), - ); - }); - - it("copies the cipher's totp code to the clipboard after filling", async () => { - const cipher1 = mock({ id: "overlay-cipher-1" }); - overlayBackground["overlayLoginCiphers"] = new Map([["overlay-cipher-1", cipher1]]); - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], - ]); - isPasswordRepromptRequiredSpy.mockResolvedValue(false); - const copyToClipboardSpy = jest - .spyOn(overlayBackground["platformUtilsService"], "copyToClipboard") - .mockImplementation(); - doAutoFillSpy.mockReturnValueOnce("totp-code"); - - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-2", - }); - await flushPromises(); - - expect(copyToClipboardSpy).toHaveBeenCalledWith("totp-code"); - }); - }); - - describe("getNewVaultItemDetails", () => { - it("will send an addNewVaultItemFromOverlay message", async () => { - jest.spyOn(BrowserApi, "tabSendMessage"); - - sendPortMessage(listPortSpy, { command: "addNewVaultItem" }); - await flushPromises(); - - expect(BrowserApi.tabSendMessage).toHaveBeenCalledWith(listPortSpy.sender.tab, { - command: "addNewVaultItemFromOverlay", - }); - }); - }); - - describe("viewSelectedCipher", () => { - let openViewVaultItemPopoutSpy: jest.SpyInstance; - - beforeEach(() => { - openViewVaultItemPopoutSpy = jest - .spyOn(overlayBackground as any, "openViewVaultItemPopout") - .mockImplementation(); - }); - - it("returns early if the passed cipher ID does not match one of the overlay login ciphers", async () => { - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-0", mock({ id: "overlay-cipher-0" })], - ]); - - sendPortMessage(listPortSpy, { - command: "viewSelectedCipher", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(openViewVaultItemPopoutSpy).not.toHaveBeenCalled(); - }); - - it("will open the view vault item popout with the selected cipher", async () => { - const cipher = mock({ id: "overlay-cipher-1" }); - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-0", mock({ id: "overlay-cipher-0" })], - ["overlay-cipher-1", cipher], - ]); - - sendPortMessage(listPortSpy, { - command: "viewSelectedCipher", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(overlayBackground["openViewVaultItemPopout"]).toHaveBeenCalledWith( - listPortSpy.sender.tab, - { - cipherId: cipher.id, - action: SHOW_AUTOFILL_BUTTON, - }, - ); - }); - }); - - describe("redirectOverlayFocusOut", () => { - it("redirects focus out of the overlay list", async () => { - const message = { - command: "redirectOverlayFocusOut", - direction: RedirectFocusDirection.Next, - }; - const redirectOverlayFocusOutSpy = jest.spyOn( - overlayBackground as any, - "redirectOverlayFocusOut", - ); - - sendPortMessage(listPortSpy, message); - await flushPromises(); - - expect(redirectOverlayFocusOutSpy).toHaveBeenCalledWith(message, listPortSpy); - }); - }); + expect(overlayBackground["inlineMenuListPort"]).toBeNull(); + expect(overlayBackground["inlineMenuButtonPort"]).toBeNull(); }); }); }); diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 2f80790134e..3b770af2004 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -1,13 +1,18 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, merge, Subject, throttleTime } from "rxjs"; +import { debounceTime, switchMap } from "rxjs/operators"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { SHOW_AUTOFILL_BUTTON } from "@bitwarden/common/autofill/constants"; +import { + AutofillOverlayVisibility, + SHOW_AUTOFILL_BUTTON, +} from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; @@ -21,80 +26,118 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window"; import { BrowserApi } from "../../platform/browser/browser-api"; import { - openViewVaultItemPopout, openAddEditVaultItemPopout, + openViewVaultItemPopout, } from "../../vault/popup/utils/vault-popout-window"; -import { AutofillService, PageDetail } from "../services/abstractions/autofill.service"; -import { AutofillOverlayElement, AutofillOverlayPort } from "../utils/autofill-overlay.enum"; +import { + AutofillOverlayElement, + AutofillOverlayPort, + MAX_SUB_FRAME_DEPTH, +} from "../enums/autofill-overlay.enum"; +import { AutofillService } from "../services/abstractions/autofill.service"; +import { generateRandomChars } from "../utils"; import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background"; import { FocusedFieldData, - OverlayBackgroundExtensionMessageHandlers, - OverlayButtonPortMessageHandlers, - OverlayCipherData, - OverlayListPortMessageHandlers, + OverlayAddNewItemMessage, OverlayBackground as OverlayBackgroundInterface, OverlayBackgroundExtensionMessage, - OverlayAddNewItemMessage, + OverlayBackgroundExtensionMessageHandlers, + InlineMenuButtonPortMessageHandlers, + InlineMenuCipherData, + InlineMenuListPortMessageHandlers, OverlayPortMessage, - WebsiteIconData, + PageDetailsForTab, + SubFrameOffsetData, + SubFrameOffsetsForTab, + CloseInlineMenuMessage, + ToggleInlineMenuHiddenMessage, } from "./abstractions/overlay.background"; -class OverlayBackground implements OverlayBackgroundInterface { +export class OverlayBackground implements OverlayBackgroundInterface { private readonly openUnlockPopout = openUnlockPopout; private readonly openViewVaultItemPopout = openViewVaultItemPopout; private readonly openAddEditVaultItemPopout = openAddEditVaultItemPopout; - private overlayLoginCiphers: Map = new Map(); - private pageDetailsForTab: Record< - chrome.runtime.MessageSender["tab"]["id"], - Map - > = {}; - private userAuthStatus: AuthenticationStatus = AuthenticationStatus.LoggedOut; - private overlayButtonPort: chrome.runtime.Port; - private overlayListPort: chrome.runtime.Port; + private pageDetailsForTab: PageDetailsForTab = {}; + private subFrameOffsetsForTab: SubFrameOffsetsForTab = {}; + private portKeyForTab: Record = {}; private expiredPorts: chrome.runtime.Port[] = []; + private inlineMenuButtonPort: chrome.runtime.Port; + private inlineMenuListPort: chrome.runtime.Port; + private inlineMenuCiphers: Map = new Map(); + private inlineMenuPageTranslations: Record; + private delayedCloseTimeout: number | NodeJS.Timeout; + private startInlineMenuFadeInSubject = new Subject(); + private cancelInlineMenuFadeInSubject = new Subject(); + private startUpdateInlineMenuPositionSubject = new Subject(); + private cancelUpdateInlineMenuPositionSubject = new Subject(); + private repositionInlineMenuSubject = new Subject(); + private rebuildSubFrameOffsetsSubject = new Subject(); private focusedFieldData: FocusedFieldData; - private overlayPageTranslations: Record; + private isFieldCurrentlyFocused: boolean = false; + private isFieldCurrentlyFilling: boolean = false; + private isInlineMenuButtonVisible: boolean = false; + private isInlineMenuListVisible: boolean = false; private iconsServerUrl: string; private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = { - openAutofillOverlay: () => this.openOverlay(false), autofillOverlayElementClosed: ({ message, sender }) => this.overlayElementClosed(message, sender), autofillOverlayAddNewVaultItem: ({ message, sender }) => this.addNewVaultItem(message, sender), - getAutofillOverlayVisibility: () => this.getOverlayVisibility(), - checkAutofillOverlayFocused: () => this.checkOverlayFocused(), - focusAutofillOverlayList: () => this.focusOverlayList(), - updateAutofillOverlayPosition: ({ message, sender }) => - this.updateOverlayPosition(message, sender), - updateAutofillOverlayHidden: ({ message }) => this.updateOverlayHidden(message), + triggerAutofillOverlayReposition: ({ sender }) => this.triggerOverlayReposition(sender), + checkIsInlineMenuCiphersPopulated: ({ sender }) => + this.checkIsInlineMenuCiphersPopulated(sender), updateFocusedFieldData: ({ message, sender }) => this.setFocusedFieldData(message, sender), + updateIsFieldCurrentlyFocused: ({ message }) => this.updateIsFieldCurrentlyFocused(message), + checkIsFieldCurrentlyFocused: () => this.checkIsFieldCurrentlyFocused(), + updateIsFieldCurrentlyFilling: ({ message }) => this.updateIsFieldCurrentlyFilling(message), + checkIsFieldCurrentlyFilling: () => this.checkIsFieldCurrentlyFilling(), + getAutofillInlineMenuVisibility: () => this.getInlineMenuVisibility(), + openAutofillInlineMenu: () => this.openInlineMenu(false), + closeAutofillInlineMenu: ({ message, sender }) => this.closeInlineMenu(sender, message), + checkAutofillInlineMenuFocused: ({ sender }) => this.checkInlineMenuFocused(sender), + focusAutofillInlineMenuList: () => this.focusInlineMenuList(), + updateAutofillInlineMenuPosition: ({ message, sender }) => + this.updateInlineMenuPosition(message, sender), + updateAutofillInlineMenuElementIsVisibleStatus: ({ message, sender }) => + this.updateInlineMenuElementIsVisibleStatus(message, sender), + checkIsAutofillInlineMenuButtonVisible: () => this.checkIsInlineMenuButtonVisible(), + checkIsAutofillInlineMenuListVisible: () => this.checkIsInlineMenuListVisible(), + getCurrentTabFrameId: ({ sender }) => this.getSenderFrameId(sender), + updateSubFrameData: ({ message, sender }) => this.updateSubFrameData(message, sender), + triggerSubFrameFocusInRebuild: ({ sender }) => this.triggerSubFrameFocusInRebuild(sender), + destroyAutofillInlineMenuListeners: ({ message, sender }) => + this.triggerDestroyInlineMenuListeners(sender.tab, message.subFrameData.frameId), collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender), unlockCompleted: ({ message }) => this.unlockCompleted(message), + doFullSync: () => this.updateOverlayCiphers(), addedCipher: () => this.updateOverlayCiphers(), addEditCipherSubmitted: () => this.updateOverlayCiphers(), editedCipher: () => this.updateOverlayCiphers(), deletedCipher: () => this.updateOverlayCiphers(), }; - private readonly overlayButtonPortMessageHandlers: OverlayButtonPortMessageHandlers = { - overlayButtonClicked: ({ port }) => this.handleOverlayButtonClicked(port), - closeAutofillOverlay: ({ port }) => this.closeOverlay(port), - forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true), - overlayPageBlurred: () => this.checkOverlayListFocused(), - redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port), + private readonly inlineMenuButtonPortMessageHandlers: InlineMenuButtonPortMessageHandlers = { + triggerDelayedAutofillInlineMenuClosure: ({ port }) => this.triggerDelayedInlineMenuClosure(), + autofillInlineMenuButtonClicked: ({ port }) => this.handleInlineMenuButtonClicked(port), + autofillInlineMenuBlurred: () => this.checkInlineMenuListFocused(), + redirectAutofillInlineMenuFocusOut: ({ message, port }) => + this.redirectInlineMenuFocusOut(message, port), + updateAutofillInlineMenuColorScheme: () => this.updateInlineMenuButtonColorScheme(), }; - private readonly overlayListPortMessageHandlers: OverlayListPortMessageHandlers = { - checkAutofillOverlayButtonFocused: () => this.checkOverlayButtonFocused(), - forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true), - overlayPageBlurred: () => this.checkOverlayButtonFocused(), + private readonly inlineMenuListPortMessageHandlers: InlineMenuListPortMessageHandlers = { + checkAutofillInlineMenuButtonFocused: () => this.checkInlineMenuButtonFocused(), + autofillInlineMenuBlurred: () => this.checkInlineMenuButtonFocused(), unlockVault: ({ port }) => this.unlockVault(port), - fillSelectedListItem: ({ message, port }) => this.fillSelectedOverlayListItem(message, port), + fillAutofillInlineMenuCipher: ({ message, port }) => this.fillInlineMenuCipher(message, port), addNewVaultItem: ({ port }) => this.getNewVaultItemDetails(port), viewSelectedCipher: ({ message, port }) => this.viewSelectedCipher(message, port), - redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port), + redirectAutofillInlineMenuFocusOut: ({ message, port }) => + this.redirectInlineMenuFocusOut(message, port), + updateAutofillInlineMenuListHeight: ({ message }) => this.updateInlineMenuListHeight(message), }; constructor( + private logService: LogService, private cipherService: CipherService, private autofillService: AutofillService, private authService: AuthService, @@ -104,7 +147,53 @@ class OverlayBackground implements OverlayBackgroundInterface { private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private themeStateService: ThemeStateService, - ) {} + ) { + this.initOverlayEventObservables(); + } + + /** + * Sets up the extension message listeners and gets the settings for the + * overlay's visibility and the user's authentication status. + */ + async init() { + this.setupExtensionListeners(); + const env = await firstValueFrom(this.environmentService.environment$); + this.iconsServerUrl = env.getIconsUrl(); + } + + /** + * Initializes event observables that handle events which affect the overlay's behavior. + */ + private initOverlayEventObservables() { + this.repositionInlineMenuSubject + .pipe( + debounceTime(1000), + switchMap((sender) => this.repositionInlineMenu(sender)), + ) + .subscribe(); + this.rebuildSubFrameOffsetsSubject + .pipe( + throttleTime(100), + switchMap((sender) => this.rebuildSubFrameOffsets(sender)), + ) + .subscribe(); + + // Debounce used to update inline menu position + merge( + this.startUpdateInlineMenuPositionSubject.pipe(debounceTime(150)), + this.cancelUpdateInlineMenuPositionSubject, + ) + .pipe(switchMap((sender) => this.updateInlineMenuPositionAfterRepositionEvent(sender))) + .subscribe(); + + // FadeIn Observable behavior + merge( + this.startInlineMenuFadeInSubject.pipe(debounceTime(150)), + this.cancelInlineMenuFadeInSubject, + ) + .pipe(switchMap((cancelSignal) => this.triggerInlineMenuFadeIn(!!cancelSignal))) + .subscribe(); + } /** * Removes cached page details for a tab @@ -113,89 +202,83 @@ class OverlayBackground implements OverlayBackgroundInterface { * @param tabId - Used to reference the page details of a specific tab */ removePageDetails(tabId: number) { - if (!this.pageDetailsForTab[tabId]) { - return; + if (this.pageDetailsForTab[tabId]) { + this.pageDetailsForTab[tabId].clear(); + delete this.pageDetailsForTab[tabId]; } - this.pageDetailsForTab[tabId].clear(); - delete this.pageDetailsForTab[tabId]; + if (this.portKeyForTab[tabId]) { + delete this.portKeyForTab[tabId]; + } } /** - * Sets up the extension message listeners and gets the settings for the - * overlay's visibility and the user's authentication status. - */ - async init() { - this.setupExtensionMessageListeners(); - const env = await firstValueFrom(this.environmentService.environment$); - this.iconsServerUrl = env.getIconsUrl(); - await this.getOverlayVisibility(); - await this.getAuthStatus(); - } - - /** - * Updates the overlay list's ciphers and sends the updated list to the overlay list iframe. + * Updates the inline menu list's ciphers and sends the updated list to the inline menu list iframe. * Queries all ciphers for the given url, and sorts them by last used. Will not update the * list of ciphers if the extension is not unlocked. */ async updateOverlayCiphers() { const authStatus = await firstValueFrom(this.authService.activeAccountStatus$); if (authStatus !== AuthenticationStatus.Unlocked) { + if (this.focusedFieldData) { + void this.closeInlineMenuAfterCiphersUpdate(); + } return; } const currentTab = await BrowserApi.getTabFromCurrentWindowId(); - if (!currentTab?.url) { - return; + if (this.focusedFieldData && currentTab?.id !== this.focusedFieldData.tabId) { + void this.closeInlineMenuAfterCiphersUpdate(); } - this.overlayLoginCiphers = new Map(); - const ciphersViews = (await this.cipherService.getAllDecryptedForUrl(currentTab.url)).sort( - (a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b), - ); + this.inlineMenuCiphers = new Map(); + const ciphersViews = ( + await this.cipherService.getAllDecryptedForUrl(currentTab?.url || "") + ).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b)); for (let cipherIndex = 0; cipherIndex < ciphersViews.length; cipherIndex++) { - this.overlayLoginCiphers.set(`overlay-cipher-${cipherIndex}`, ciphersViews[cipherIndex]); + this.inlineMenuCiphers.set(`inline-menu-cipher-${cipherIndex}`, ciphersViews[cipherIndex]); } - const ciphers = await this.getOverlayCipherData(); - this.overlayListPort?.postMessage({ command: "updateOverlayListCiphers", ciphers }); - await BrowserApi.tabSendMessageData(currentTab, "updateIsOverlayCiphersPopulated", { - isOverlayCiphersPopulated: Boolean(ciphers.length), + const ciphers = await this.getInlineMenuCipherData(); + this.inlineMenuListPort?.postMessage({ + command: "updateAutofillInlineMenuListCiphers", + ciphers, }); } /** * Strips out unnecessary data from the ciphers and returns an array of - * objects that contain the cipher data needed for the overlay list. + * objects that contain the cipher data needed for the inline menu list. */ - private async getOverlayCipherData(): Promise { + private async getInlineMenuCipherData(): Promise { const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$); - const overlayCiphersArray = Array.from(this.overlayLoginCiphers); - const overlayCipherData: OverlayCipherData[] = []; - let loginCipherIcon: WebsiteIconData; + const inlineMenuCiphersArray = Array.from(this.inlineMenuCiphers); + const inlineMenuCipherData: InlineMenuCipherData[] = []; - for (let cipherIndex = 0; cipherIndex < overlayCiphersArray.length; cipherIndex++) { - const [overlayCipherId, cipher] = overlayCiphersArray[cipherIndex]; - if (!loginCipherIcon && cipher.type === CipherType.Login) { - loginCipherIcon = buildCipherIcon(this.iconsServerUrl, cipher, showFavicons); - } + for (let cipherIndex = 0; cipherIndex < inlineMenuCiphersArray.length; cipherIndex++) { + const [inlineMenuCipherId, cipher] = inlineMenuCiphersArray[cipherIndex]; - overlayCipherData.push({ - id: overlayCipherId, + inlineMenuCipherData.push({ + id: inlineMenuCipherId, name: cipher.name, type: cipher.type, reprompt: cipher.reprompt, favorite: cipher.favorite, - icon: - cipher.type === CipherType.Login - ? loginCipherIcon - : buildCipherIcon(this.iconsServerUrl, cipher, showFavicons), + icon: buildCipherIcon(this.iconsServerUrl, cipher, showFavicons), login: cipher.type === CipherType.Login ? { username: cipher.login.username } : null, card: cipher.type === CipherType.Card ? cipher.card.subTitle : null, }); } - return overlayCipherData; + return inlineMenuCipherData; + } + + /** + * Gets the currently focused field and closes the inline menu on that tab. + */ + private async closeInlineMenuAfterCiphersUpdate() { + const focusedFieldTab = await BrowserApi.getTab(this.focusedFieldData.tabId); + this.closeInlineMenu({ tab: focusedFieldTab }, { forceCloseInlineMenu: true }); } /** @@ -215,6 +298,13 @@ class OverlayBackground implements OverlayBackgroundInterface { details: message.details, }; + if (pageDetails.frameId !== 0 && pageDetails.details.fields.length) { + void this.buildSubFrameOffsets(pageDetails.tab, pageDetails.frameId, pageDetails.details.url); + void BrowserApi.tabSendMessage(pageDetails.tab, { + command: "setupRebuildSubFrameOffsetsListeners", + }); + } + const pageDetailsMap = this.pageDetailsForTab[sender.tab.id]; if (!pageDetailsMap) { this.pageDetailsForTab[sender.tab.id] = new Map([[sender.frameId, pageDetails]]); @@ -225,22 +315,205 @@ class OverlayBackground implements OverlayBackgroundInterface { } /** - * Triggers autofill for the selected cipher in the overlay list. Also places - * the selected cipher at the top of the list of ciphers. + * Returns the frameId, called when calculating sub frame offsets within the tab. + * Is used to determine if we should reposition the inline menu when a resize event + * occurs within a frame. * - * @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID. - * @param sender - The sender of the port message + * @param sender - The sender of the message */ - private async fillSelectedOverlayListItem( - { overlayCipherId }: OverlayPortMessage, - { sender }: chrome.runtime.Port, + private getSenderFrameId(sender: chrome.runtime.MessageSender) { + return sender.frameId; + } + + /** + * Handles sub frame offset calculations for the given tab and frame id. + * Is used in setting the position of the inline menu list and button. + * + * @param message - The message received from the `updateSubFrameData` command + * @param sender - The sender of the message + */ + private updateSubFrameData( + message: OverlayBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, ) { - const pageDetails = this.pageDetailsForTab[sender.tab.id]; - if (!overlayCipherId || !pageDetails?.size) { + const subFrameOffsetsForTab = this.subFrameOffsetsForTab[sender.tab.id]; + if (subFrameOffsetsForTab) { + subFrameOffsetsForTab.set(message.subFrameData.frameId, message.subFrameData); + } + } + + /** + * Builds the offset data for a sub frame of a tab. The offset data is used + * to calculate the position of the inline menu list and button. + * + * @param tab - The tab that the sub frame is associated with + * @param frameId - The frame ID of the sub frame + * @param url - The URL of the sub frame + * @param forceRebuild - Identifies whether the sub frame offsets should be rebuilt + */ + private async buildSubFrameOffsets( + tab: chrome.tabs.Tab, + frameId: number, + url: string, + forceRebuild: boolean = false, + ) { + let subFrameDepth = 0; + const tabId = tab.id; + let subFrameOffsetsForTab = this.subFrameOffsetsForTab[tabId]; + if (!subFrameOffsetsForTab) { + this.subFrameOffsetsForTab[tabId] = new Map(); + subFrameOffsetsForTab = this.subFrameOffsetsForTab[tabId]; + } + + if (!forceRebuild && subFrameOffsetsForTab.get(frameId)) { return; } - const cipher = this.overlayLoginCiphers.get(overlayCipherId); + const subFrameData: SubFrameOffsetData = { url, top: 0, left: 0, parentFrameIds: [0] }; + let frameDetails = await BrowserApi.getFrameDetails({ tabId, frameId }); + + while (frameDetails && frameDetails.parentFrameId > -1) { + subFrameDepth++; + if (subFrameDepth >= MAX_SUB_FRAME_DEPTH) { + subFrameOffsetsForTab.set(frameId, null); + this.triggerDestroyInlineMenuListeners(tab, frameId); + return; + } + + const subFrameOffset: SubFrameOffsetData = await BrowserApi.tabSendMessage( + tab, + { + command: "getSubFrameOffsets", + subFrameUrl: frameDetails.url, + subFrameId: frameDetails.documentId, + }, + { frameId: frameDetails.parentFrameId }, + ); + + if (!subFrameOffset) { + subFrameOffsetsForTab.set(frameId, null); + void BrowserApi.tabSendMessage( + tab, + { command: "getSubFrameOffsetsFromWindowMessage", subFrameId: frameId }, + { frameId }, + ); + return; + } + + subFrameData.top += subFrameOffset.top; + subFrameData.left += subFrameOffset.left; + if (!subFrameData.parentFrameIds.includes(frameDetails.parentFrameId)) { + subFrameData.parentFrameIds.push(frameDetails.parentFrameId); + } + + frameDetails = await BrowserApi.getFrameDetails({ + tabId, + frameId: frameDetails.parentFrameId, + }); + } + + subFrameOffsetsForTab.set(frameId, subFrameData); + } + + /** + * Triggers a removal and destruction of all + * + * @param tab - The tab that the sub frame is associated with + * @param frameId - The frame ID of the sub frame + */ + private triggerDestroyInlineMenuListeners(tab: chrome.tabs.Tab, frameId: number) { + this.logService.error( + "Excessive frame depth encountered, destroying inline menu on field within frame", + tab, + frameId, + ); + + void BrowserApi.tabSendMessage( + tab, + { command: "destroyAutofillInlineMenuListeners" }, + { frameId }, + ); + } + + /** + * Rebuilds the sub frame offsets for the tab associated with the sender. + * + * @param sender - The sender of the message + */ + private async rebuildSubFrameOffsets(sender: chrome.runtime.MessageSender) { + this.cancelUpdateInlineMenuPositionSubject.next(); + this.clearDelayedInlineMenuClosure(); + + const subFrameOffsetsForTab = this.subFrameOffsetsForTab[sender.tab.id]; + if (subFrameOffsetsForTab) { + const tabFrameIds = Array.from(subFrameOffsetsForTab.keys()); + for (const frameId of tabFrameIds) { + await this.buildSubFrameOffsets(sender.tab, frameId, sender.url, true); + } + } + } + + /** + * Handles updating the inline menu's position after rebuilding the sub frames + * for the provided tab. Will skip repositioning the inline menu if the field + * is not currently focused, or if the focused field has a value. + * + * @param sender - The sender of the message + */ + private async updateInlineMenuPositionAfterRepositionEvent( + sender: chrome.runtime.MessageSender | void, + ) { + if (!sender || !this.isFieldCurrentlyFocused) { + return; + } + + if (!this.checkIsInlineMenuButtonVisible()) { + void this.toggleInlineMenuHidden( + { isInlineMenuHidden: false, setTransparentInlineMenu: true }, + sender, + ); + } + + void this.updateInlineMenuPosition({ overlayElement: AutofillOverlayElement.Button }, sender); + + const mostRecentlyFocusedFieldHasValue = await BrowserApi.tabSendMessage( + sender.tab, + { command: "checkMostRecentlyFocusedFieldHasValue" }, + { frameId: this.focusedFieldData?.frameId }, + ); + + if ((await this.getInlineMenuVisibility()) === AutofillOverlayVisibility.OnButtonClick) { + return; + } + + if ( + mostRecentlyFocusedFieldHasValue && + (this.checkIsInlineMenuCiphersPopulated(sender) || + (await this.getAuthStatus()) !== AuthenticationStatus.Unlocked) + ) { + return; + } + + void this.updateInlineMenuPosition({ overlayElement: AutofillOverlayElement.List }, sender); + } + + /** + * Triggers autofill for the selected cipher in the inline menu list. Also places + * the selected cipher at the top of the list of ciphers. + * + * @param inlineMenuCipherId - Cipher ID corresponding to the inlineMenuCiphers map. Does not correspond to the actual cipher's ID. + * @param sender - The sender of the port message + */ + private async fillInlineMenuCipher( + { inlineMenuCipherId }: OverlayPortMessage, + { sender }: chrome.runtime.Port, + ) { + const pageDetails = this.pageDetailsForTab[sender.tab.id]; + if (!inlineMenuCipherId || !pageDetails?.size) { + return; + } + + const cipher = this.inlineMenuCiphers.get(inlineMenuCipherId); if (await this.autofillService.isPasswordRepromptRequired(cipher, sender.tab)) { return; @@ -257,47 +530,117 @@ class OverlayBackground implements OverlayBackgroundInterface { this.platformUtilsService.copyToClipboard(totpCode); } - this.overlayLoginCiphers = new Map([[overlayCipherId, cipher], ...this.overlayLoginCiphers]); + this.inlineMenuCiphers = new Map([[inlineMenuCipherId, cipher], ...this.inlineMenuCiphers]); } /** - * Checks if the overlay is focused. Will check the overlay list - * if it is open, otherwise it will check the overlay button. + * Checks if the inline menu is focused. Will check the inline menu list + * if it is open, otherwise it will check the inline menu button. */ - private checkOverlayFocused() { - if (this.overlayListPort) { - this.checkOverlayListFocused(); + private checkInlineMenuFocused(sender: chrome.runtime.MessageSender) { + if (!this.senderTabHasFocusedField(sender)) { + return; + } + + if (this.inlineMenuListPort) { + this.checkInlineMenuListFocused(); return; } - this.checkOverlayButtonFocused(); + this.checkInlineMenuButtonFocused(); } /** - * Posts a message to the overlay button iframe to check if it is focused. + * Posts a message to the inline menu button iframe to check if it is focused. */ - private checkOverlayButtonFocused() { - this.overlayButtonPort?.postMessage({ command: "checkAutofillOverlayButtonFocused" }); + private checkInlineMenuButtonFocused() { + this.inlineMenuButtonPort?.postMessage({ command: "checkAutofillInlineMenuButtonFocused" }); } /** - * Posts a message to the overlay list iframe to check if it is focused. + * Posts a message to the inline menu list iframe to check if it is focused. */ - private checkOverlayListFocused() { - this.overlayListPort?.postMessage({ command: "checkAutofillOverlayListFocused" }); + private checkInlineMenuListFocused() { + this.inlineMenuListPort?.postMessage({ command: "checkAutofillInlineMenuListFocused" }); } /** - * Sends a message to the sender tab to close the autofill overlay. + * Sends a message to the sender tab to close the autofill inline menu. * * @param sender - The sender of the port message - * @param forceCloseOverlay - Identifies whether the overlay should be force closed + * @param forceCloseInlineMenu - Identifies whether the inline menu should be forced closed + * @param overlayElement - The overlay element to close, either the list or button */ - private closeOverlay({ sender }: chrome.runtime.Port, forceCloseOverlay = false) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.tabSendMessageData(sender.tab, "closeAutofillOverlay", { forceCloseOverlay }); + private closeInlineMenu( + sender: chrome.runtime.MessageSender, + { forceCloseInlineMenu, overlayElement }: CloseInlineMenuMessage = {}, + ) { + const command = "closeAutofillInlineMenu"; + const sendOptions = { frameId: 0 }; + if (forceCloseInlineMenu) { + void BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions); + this.isInlineMenuButtonVisible = false; + this.isInlineMenuListVisible = false; + return; + } + + if (this.isFieldCurrentlyFocused) { + return; + } + + if (this.isFieldCurrentlyFilling) { + void BrowserApi.tabSendMessage( + sender.tab, + { command, overlayElement: AutofillOverlayElement.List }, + sendOptions, + ); + this.isInlineMenuListVisible = false; + return; + } + + if (overlayElement === AutofillOverlayElement.Button) { + this.isInlineMenuButtonVisible = false; + } + + if (overlayElement === AutofillOverlayElement.List) { + this.isInlineMenuListVisible = false; + } + + if (!overlayElement) { + this.isInlineMenuButtonVisible = false; + this.isInlineMenuListVisible = false; + } + + void BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions); + } + + /** + * Sends a message to the sender tab to trigger a delayed closure of the inline menu. + * This is used to ensure that we capture click events on the inline menu in the case + * that some on page programmatic method attempts to force focus redirection. + */ + private triggerDelayedInlineMenuClosure() { + if (this.isFieldCurrentlyFocused) { + return; + } + + this.clearDelayedInlineMenuClosure(); + this.delayedCloseTimeout = globalThis.setTimeout(() => { + const message = { command: "triggerDelayedAutofillInlineMenuClosure" }; + this.inlineMenuButtonPort?.postMessage(message); + this.inlineMenuListPort?.postMessage(message); + }, 100); + } + + /** + * Clears the delayed closure timeout for the inline menu, effectively + * cancelling the event from occurring. + */ + private clearDelayedInlineMenuClosure() { + if (this.delayedCloseTimeout) { + clearTimeout(this.delayedCloseTimeout); + } } /** @@ -311,61 +654,141 @@ class OverlayBackground implements OverlayBackgroundInterface { { overlayElement }: OverlayBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ) { - if (sender.tab.id !== this.focusedFieldData?.tabId) { + if (!this.senderTabHasFocusedField(sender)) { this.expiredPorts.forEach((port) => port.disconnect()); this.expiredPorts = []; + return; } if (overlayElement === AutofillOverlayElement.Button) { - this.overlayButtonPort?.disconnect(); - this.overlayButtonPort = null; + this.inlineMenuButtonPort?.disconnect(); + this.inlineMenuButtonPort = null; + this.isInlineMenuButtonVisible = false; return; } - this.overlayListPort?.disconnect(); - this.overlayListPort = null; + this.inlineMenuListPort?.disconnect(); + this.inlineMenuListPort = null; + this.isInlineMenuListVisible = false; } /** - * Updates the position of either the overlay list or button. The position + * Updates the position of either the inline menu list or button. The position * is based on the focused field's position and dimensions. * * @param overlayElement - The overlay element to update, either the list or button * @param sender - The sender of the port message */ - private updateOverlayPosition( + private async updateInlineMenuPosition( { overlayElement }: { overlayElement?: string }, sender: chrome.runtime.MessageSender, ) { - if (!overlayElement || sender.tab.id !== this.focusedFieldData?.tabId) { + if (!overlayElement || !this.senderTabHasFocusedField(sender)) { return; } + this.cancelInlineMenuFadeInAndPositionUpdate(); + + await BrowserApi.tabSendMessage( + sender.tab, + { command: "appendAutofillInlineMenuToDom", overlayElement }, + { frameId: 0 }, + ); + + const subFrameOffsetsForTab = this.subFrameOffsetsForTab[this.focusedFieldData.tabId]; + let subFrameOffsets: SubFrameOffsetData; + if (subFrameOffsetsForTab) { + subFrameOffsets = subFrameOffsetsForTab.get(this.focusedFieldData.frameId); + if (subFrameOffsets === null) { + this.rebuildSubFrameOffsetsSubject.next(sender); + this.startUpdateInlineMenuPositionSubject.next(sender); + return; + } + } + if (overlayElement === AutofillOverlayElement.Button) { - this.overlayButtonPort?.postMessage({ - command: "updateIframePosition", - styles: this.getOverlayButtonPosition(), + this.inlineMenuButtonPort?.postMessage({ + command: "updateAutofillInlineMenuPosition", + styles: this.getInlineMenuButtonPosition(subFrameOffsets), }); + this.startInlineMenuFadeIn(); return; } - this.overlayListPort?.postMessage({ - command: "updateIframePosition", - styles: this.getOverlayListPosition(), + this.inlineMenuListPort?.postMessage({ + command: "updateAutofillInlineMenuPosition", + styles: this.getInlineMenuListPosition(subFrameOffsets), }); + this.startInlineMenuFadeIn(); + } + + /** + * Triggers an update of the inline menu's visibility after the top level frame + * appends the element to the DOM. + * + * @param message - The message received from the content script + * @param sender - The sender of the port message + */ + private updateInlineMenuElementIsVisibleStatus( + message: OverlayBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + if (!this.senderTabHasFocusedField(sender)) { + return; + } + + const { overlayElement, isVisible } = message; + if (overlayElement === AutofillOverlayElement.Button) { + this.isInlineMenuButtonVisible = isVisible; + return; + } + + if (overlayElement === AutofillOverlayElement.List) { + this.isInlineMenuListVisible = isVisible; + } + } + + /** + * Handles updating the opacity of both the inline menu button and list. + * This is used to simultaneously fade in the inline menu elements. + */ + private startInlineMenuFadeIn() { + this.cancelInlineMenuFadeIn(); + this.startInlineMenuFadeInSubject.next(); + } + + /** + * Clears the timeout used to fade in the inline menu elements. + */ + private cancelInlineMenuFadeIn() { + this.cancelInlineMenuFadeInSubject.next(true); + } + + /** + * Posts a message to the inline menu elements to trigger a fade in of the inline menu. + * + * @param cancelFadeIn - Signal passed to debounced observable to cancel the fade in + */ + private async triggerInlineMenuFadeIn(cancelFadeIn: boolean = false) { + if (cancelFadeIn) { + return; + } + + const message = { command: "fadeInAutofillInlineMenuIframe" }; + this.inlineMenuButtonPort?.postMessage(message); + this.inlineMenuListPort?.postMessage(message); } /** * Gets the position of the focused field and calculates the position - * of the overlay button based on the focused field's position and dimensions. + * of the inline menu button based on the focused field's position and dimensions. */ - private getOverlayButtonPosition() { - if (!this.focusedFieldData) { - return; - } + private getInlineMenuButtonPosition(subFrameOffsets: SubFrameOffsetData) { + const subFrameTopOffset = subFrameOffsets?.top || 0; + const subFrameLeftOffset = subFrameOffsets?.left || 0; const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; const { paddingRight, paddingLeft } = this.focusedFieldData.focusedFieldStyles; @@ -374,15 +797,15 @@ class OverlayBackground implements OverlayBackgroundInterface { elementOffset = height >= 50 ? height * 0.47 : height * 0.42; } - const elementHeight = height - elementOffset; - const elementTopPosition = top + elementOffset / 2; - let elementLeftPosition = left + width - height + elementOffset / 2; - const fieldPaddingRight = parseInt(paddingRight, 10); const fieldPaddingLeft = parseInt(paddingLeft, 10); - if (fieldPaddingRight > fieldPaddingLeft) { - elementLeftPosition = left + width - height - (fieldPaddingRight - elementOffset + 2); - } + const elementHeight = height - elementOffset; + + const elementTopPosition = subFrameTopOffset + top + elementOffset / 2; + const elementLeftPosition = + fieldPaddingRight > fieldPaddingLeft + ? subFrameLeftOffset + left + width - height - (fieldPaddingRight - elementOffset + 2) + : subFrameLeftOffset + left + width - height + elementOffset / 2; return { top: `${Math.round(elementTopPosition)}px`, @@ -394,18 +817,17 @@ class OverlayBackground implements OverlayBackgroundInterface { /** * Gets the position of the focused field and calculates the position - * of the overlay list based on the focused field's position and dimensions. + * of the inline menu list based on the focused field's position and dimensions. */ - private getOverlayListPosition() { - if (!this.focusedFieldData) { - return; - } + private getInlineMenuListPosition(subFrameOffsets: SubFrameOffsetData) { + const subFrameTopOffset = subFrameOffsets?.top || 0; + const subFrameLeftOffset = subFrameOffsets?.left || 0; const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; return { width: `${Math.round(width)}px`, - top: `${Math.round(top + height)}px`, - left: `${Math.round(left)}px`, + top: `${Math.round(top + height + subFrameTopOffset)}px`, + left: `${Math.round(left + subFrameLeftOffset)}px`, }; } @@ -419,109 +841,137 @@ class OverlayBackground implements OverlayBackgroundInterface { { focusedFieldData }: OverlayBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ) { - this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id }; + if (this.focusedFieldData?.frameId && this.focusedFieldData.frameId !== sender.frameId) { + void BrowserApi.tabSendMessage( + sender.tab, + { command: "unsetMostRecentlyFocusedField" }, + { frameId: this.focusedFieldData.frameId }, + ); + } + + this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id, frameId: sender.frameId }; } /** - * Updates the overlay's visibility based on the display property passed in the extension message. + * Updates the inline menu's visibility based on the display property passed in the extension message. * - * @param display - The display property of the overlay, either "block" or "none" + * @param display - The display property of the inline menu, either "block" or "none" + * @param sender - The sender of the extension message */ - private updateOverlayHidden({ display }: OverlayBackgroundExtensionMessage) { - if (!display) { + private async toggleInlineMenuHidden( + { isInlineMenuHidden, setTransparentInlineMenu }: ToggleInlineMenuHiddenMessage, + sender: chrome.runtime.MessageSender, + ) { + if (!this.senderTabHasFocusedField(sender)) { return; } - const portMessage = { command: "updateOverlayHidden", styles: { display } }; + this.cancelInlineMenuFadeIn(); + const display = isInlineMenuHidden ? "none" : "block"; + let styles: { display: string; opacity?: string } = { display }; - this.overlayButtonPort?.postMessage(portMessage); - this.overlayListPort?.postMessage(portMessage); + if (typeof setTransparentInlineMenu !== "undefined") { + const opacity = setTransparentInlineMenu ? "0" : "1"; + styles = { ...styles, opacity }; + } + + const portMessage = { command: "toggleAutofillInlineMenuHidden", styles }; + if (this.inlineMenuButtonPort) { + this.isInlineMenuButtonVisible = !isInlineMenuHidden; + this.inlineMenuButtonPort.postMessage(portMessage); + } + + if (this.inlineMenuListPort) { + this.isInlineMenuListVisible = !isInlineMenuHidden; + this.inlineMenuListPort.postMessage(portMessage); + } + + if (setTransparentInlineMenu) { + this.startInlineMenuFadeIn(); + } } /** - * Sends a message to the currently active tab to open the autofill overlay. + * Sends a message to the currently active tab to open the autofill inline menu. * - * @param isFocusingFieldElement - Identifies whether the field element should be focused when the overlay is opened - * @param isOpeningFullOverlay - Identifies whether the full overlay should be forced open regardless of other states + * @param isFocusingFieldElement - Identifies whether the field element should be focused when the inline menu is opened + * @param isOpeningFullInlineMenu - Identifies whether the full inline menu should be forced open regardless of other states */ - private async openOverlay(isFocusingFieldElement = false, isOpeningFullOverlay = false) { + private async openInlineMenu(isFocusingFieldElement = false, isOpeningFullInlineMenu = false) { + this.clearDelayedInlineMenuClosure(); const currentTab = await BrowserApi.getTabFromCurrentWindowId(); - await BrowserApi.tabSendMessageData(currentTab, "openAutofillOverlay", { - isFocusingFieldElement, - isOpeningFullOverlay, + await BrowserApi.tabSendMessage( + currentTab, + { + command: "openAutofillInlineMenu", + isFocusingFieldElement, + isOpeningFullInlineMenu, + authStatus: await this.getAuthStatus(), + }, + { + frameId: + this.focusedFieldData?.tabId === currentTab?.id ? this.focusedFieldData.frameId : 0, + }, + ); + } + + /** + * Gets the inline menu's visibility setting from the settings service. + */ + private async getInlineMenuVisibility(): Promise { + return await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$); + } + + /** + * Gets the user's authentication status from the auth service. If the user's authentication + * status has changed, the inline menu button's authentication status will be updated + * and the inline menu list's ciphers will be updated. + */ + private async getAuthStatus() { + return await firstValueFrom(this.authService.activeAccountStatus$); + } + + /** + * Sends a message to the inline menu button to update its authentication status. + */ + private async updateInlineMenuButtonAuthStatus() { + this.inlineMenuButtonPort?.postMessage({ + command: "updateInlineMenuButtonAuthStatus", authStatus: await this.getAuthStatus(), }); } /** - * Gets the overlay's visibility setting from the settings service. - */ - private async getOverlayVisibility(): Promise { - return await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$); - } - - /** - * Gets the user's authentication status from the auth service. If the user's - * authentication status has changed, the overlay button's authentication status - * will be updated and the overlay list's ciphers will be updated. - */ - private async getAuthStatus() { - const formerAuthStatus = this.userAuthStatus; - this.userAuthStatus = await this.authService.getAuthStatus(); - - if ( - this.userAuthStatus !== formerAuthStatus && - this.userAuthStatus === AuthenticationStatus.Unlocked - ) { - this.updateOverlayButtonAuthStatus(); - await this.updateOverlayCiphers(); - } - - return this.userAuthStatus; - } - - /** - * Sends a message to the overlay button to update its authentication status. - */ - private updateOverlayButtonAuthStatus() { - this.overlayButtonPort?.postMessage({ - command: "updateOverlayButtonAuthStatus", - authStatus: this.userAuthStatus, - }); - } - - /** - * Handles the overlay button being clicked. If the user is not authenticated, - * the vault will be unlocked. If the user is authenticated, the overlay will + * Handles the inline menu button being clicked. If the user is not authenticated, + * the vault will be unlocked. If the user is authenticated, the inline menu will * be opened. * - * @param port - The port of the overlay button + * @param port - The port of the inline menu button */ - private handleOverlayButtonClicked(port: chrome.runtime.Port) { - if (this.userAuthStatus !== AuthenticationStatus.Unlocked) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.unlockVault(port); + private async handleInlineMenuButtonClicked(port: chrome.runtime.Port) { + this.clearDelayedInlineMenuClosure(); + this.cancelInlineMenuFadeInAndPositionUpdate(); + + if ((await this.getAuthStatus()) !== AuthenticationStatus.Unlocked) { + await this.unlockVault(port); return; } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.openOverlay(false, true); + await this.openInlineMenu(false, true); } /** * Facilitates opening the unlock popout window. * - * @param port - The port of the overlay list + * @param port - The port of the inline menu list */ private async unlockVault(port: chrome.runtime.Port) { const { sender } = port; - this.closeOverlay(port); + this.closeInlineMenu(port.sender); const retryMessage: LockedVaultPendingNotificationsData = { - commandToRetry: { message: { command: "openAutofillOverlay" }, sender }, + commandToRetry: { message: { command: "openAutofillInlineMenu" }, sender }, target: "overlay.background", }; await BrowserApi.tabSendMessageData( @@ -535,18 +985,19 @@ class OverlayBackground implements OverlayBackgroundInterface { /** * Triggers the opening of a vault item popout window associated * with the passed cipher ID. - * @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID. + * @param inlineMenuCipherId - Cipher ID corresponding to the inlineMenuCiphers map. Does not correspond to the actual cipher's ID. * @param sender - The sender of the port message */ private async viewSelectedCipher( - { overlayCipherId }: OverlayPortMessage, + { inlineMenuCipherId }: OverlayPortMessage, { sender }: chrome.runtime.Port, ) { - const cipher = this.overlayLoginCiphers.get(overlayCipherId); + const cipher = this.inlineMenuCiphers.get(inlineMenuCipherId); if (!cipher) { return; } + this.closeInlineMenu(sender); await this.openViewVaultItemPopout(sender.tab, { cipherId: cipher.id, action: SHOW_AUTOFILL_BUTTON, @@ -554,32 +1005,33 @@ class OverlayBackground implements OverlayBackgroundInterface { } /** - * Facilitates redirecting focus to the overlay list. + * Facilitates redirecting focus to the inline menu list. */ - private focusOverlayList() { - this.overlayListPort?.postMessage({ command: "focusOverlayList" }); + private focusInlineMenuList() { + this.inlineMenuListPort?.postMessage({ command: "focusAutofillInlineMenuList" }); } /** - * Updates the authentication status for the user and opens the overlay if + * Updates the authentication status for the user and opens the inline menu if * a followup command is present in the message. * * @param message - Extension message received from the `unlockCompleted` command */ private async unlockCompleted(message: OverlayBackgroundExtensionMessage) { - await this.getAuthStatus(); + await this.updateInlineMenuButtonAuthStatus(); + await this.updateOverlayCiphers(); - if (message.data?.commandToRetry?.message?.command === "openAutofillOverlay") { - await this.openOverlay(true); + if (message.data?.commandToRetry?.message?.command === "openAutofillInlineMenu") { + await this.openInlineMenu(true); } } /** - * Gets the translations for the overlay page. + * Gets the translations for the inline menu page. */ - private getTranslations() { - if (!this.overlayPageTranslations) { - this.overlayPageTranslations = { + private getInlineMenuTranslations() { + if (!this.inlineMenuPageTranslations) { + this.inlineMenuPageTranslations = { locale: BrowserApi.getUILanguage(), opensInANewWindow: this.i18nService.translate("opensInANewWindow"), buttonPageTitle: this.i18nService.translate("bitwardenOverlayButton"), @@ -588,7 +1040,7 @@ class OverlayBackground implements OverlayBackgroundInterface { unlockYourAccount: this.i18nService.translate("unlockYourAccountToViewMatchingLogins"), unlockAccount: this.i18nService.translate("unlockAccount"), fillCredentialsFor: this.i18nService.translate("fillCredentialsFor"), - partialUsername: this.i18nService.translate("partialUsername"), + username: this.i18nService.translate("username")?.toLowerCase(), view: this.i18nService.translate("view"), noItemsToShow: this.i18nService.translate("noItemsToShow"), newItem: this.i18nService.translate("newItem"), @@ -596,17 +1048,17 @@ class OverlayBackground implements OverlayBackgroundInterface { }; } - return this.overlayPageTranslations; + return this.inlineMenuPageTranslations; } /** * Facilitates redirecting focus out of one of the - * overlay elements to elements on the page. + * inline menu elements to elements on the page. * * @param direction - The direction to redirect focus to (either "next", "previous" or "current) * @param sender - The sender of the port message */ - private redirectOverlayFocusOut( + private redirectInlineMenuFocusOut( { direction }: OverlayPortMessage, { sender }: chrome.runtime.Port, ) { @@ -614,9 +1066,9 @@ class OverlayBackground implements OverlayBackgroundInterface { return; } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.tabSendMessageData(sender.tab, "redirectOverlayFocusOut", { direction }); + void BrowserApi.tabSendMessageData(sender.tab, "redirectAutofillInlineMenuFocusOut", { + direction, + }); } /** @@ -626,7 +1078,17 @@ class OverlayBackground implements OverlayBackgroundInterface { * @param sender - The sender of the port message */ private getNewVaultItemDetails({ sender }: chrome.runtime.Port) { - void BrowserApi.tabSendMessage(sender.tab, { command: "addNewVaultItemFromOverlay" }); + if (!this.senderTabHasFocusedField(sender)) { + return; + } + + void BrowserApi.tabSendMessage( + sender.tab, + { command: "addNewVaultItemFromOverlay" }, + { + frameId: this.focusedFieldData.frameId || 0, + }, + ); } /** @@ -644,6 +1106,7 @@ class OverlayBackground implements OverlayBackgroundInterface { return; } + this.closeInlineMenu(sender); const uriView = new LoginUriView(); uriView.uri = login.uri; @@ -667,11 +1130,222 @@ class OverlayBackground implements OverlayBackgroundInterface { await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher"); } + /** + * Updates the property that identifies if a form field set up for the inline menu is currently focused. + * + * @param message - The message received from the web page + */ + private updateIsFieldCurrentlyFocused(message: OverlayBackgroundExtensionMessage) { + this.isFieldCurrentlyFocused = message.isFieldCurrentlyFocused; + } + + /** + * Allows a content script to check if a form field setup for the inline menu is currently focused. + */ + private checkIsFieldCurrentlyFocused() { + return this.isFieldCurrentlyFocused; + } + + /** + * Updates the property that identifies if a form field is currently being autofilled. + * + * @param message - The message received from the web page + */ + private updateIsFieldCurrentlyFilling(message: OverlayBackgroundExtensionMessage) { + this.isFieldCurrentlyFilling = message.isFieldCurrentlyFilling; + } + + /** + * Allows a content script to check if a form field is currently being autofilled. + */ + private checkIsFieldCurrentlyFilling() { + return this.isFieldCurrentlyFilling; + } + + /** + * Returns the visibility status of the inline menu button. + */ + private checkIsInlineMenuButtonVisible(): boolean { + return this.isInlineMenuButtonVisible; + } + + /** + * Returns the visibility status of the inline menu list. + */ + private checkIsInlineMenuListVisible(): boolean { + return this.isInlineMenuListVisible; + } + + /** + * Responds to the content script's request to check if the inline menu ciphers are populated. + * This will return true only if the sender is the focused field's tab and the inline menu + * ciphers are populated. + * + * @param sender - The sender of the message + */ + private checkIsInlineMenuCiphersPopulated(sender: chrome.runtime.MessageSender) { + return this.senderTabHasFocusedField(sender) && this.inlineMenuCiphers.size > 0; + } + + /** + * Triggers an update in the meta "color-scheme" value within the inline menu button. + * This is done to ensure that the button element has a transparent background, which + * is accomplished by setting the "color-scheme" meta value of the button iframe to + * the same value as the page's meta "color-scheme" value. + */ + private updateInlineMenuButtonColorScheme() { + this.inlineMenuButtonPort?.postMessage({ + command: "updateAutofillInlineMenuColorScheme", + }); + } + + /** + * Triggers an update in the inline menu list's height. + * + * @param message - Contains the dimensions of the inline menu list + */ + private updateInlineMenuListHeight(message: OverlayBackgroundExtensionMessage) { + this.inlineMenuListPort?.postMessage({ + command: "updateAutofillInlineMenuPosition", + styles: message.styles, + }); + } + + /** + * Handles verifying whether the inline menu should be repositioned. This is used to + * guard against removing the inline menu when other frames trigger a resize event. + * + * @param sender - The sender of the message + */ + private checkShouldRepositionInlineMenu(sender: chrome.runtime.MessageSender): boolean { + if (!this.focusedFieldData || !this.senderTabHasFocusedField(sender)) { + return false; + } + + if (this.focusedFieldData?.frameId === sender.frameId) { + return true; + } + + const subFrameOffsetsForTab = this.subFrameOffsetsForTab[sender.tab.id]; + if (subFrameOffsetsForTab) { + for (const value of subFrameOffsetsForTab.values()) { + if (value?.parentFrameIds.includes(sender.frameId)) { + return true; + } + } + } + + return false; + } + + /** + * Identifies if the sender tab is the same as the focused field's tab. + * + * @param sender - The sender of the message + */ + private senderTabHasFocusedField(sender: chrome.runtime.MessageSender) { + return sender.tab.id === this.focusedFieldData?.tabId; + } + + /** + * Triggers when a scroll or resize event occurs within a tab. Will reposition the inline menu + * if the focused field is within the viewport. + * + * @param sender - The sender of the message + */ + private async triggerOverlayReposition(sender: chrome.runtime.MessageSender) { + if (!this.checkShouldRepositionInlineMenu(sender)) { + return; + } + + this.resetFocusedFieldSubFrameOffsets(sender); + this.cancelInlineMenuFadeInAndPositionUpdate(); + void this.toggleInlineMenuHidden({ isInlineMenuHidden: true }, sender); + this.repositionInlineMenuSubject.next(sender); + } + + /** + * Sets the sub frame offsets for the currently focused field's frame to a null value . + * This ensures that we can delay presentation of the inline menu after a reposition + * event if the user clicks on a field before the sub frames can be rebuilt. + * + * @param sender + */ + private resetFocusedFieldSubFrameOffsets(sender: chrome.runtime.MessageSender) { + if (this.focusedFieldData.frameId > 0 && this.subFrameOffsetsForTab[sender.tab.id]) { + this.subFrameOffsetsForTab[sender.tab.id].set(this.focusedFieldData.frameId, null); + } + } + + /** + * Triggers when a focus event occurs within a tab. Will reposition the inline menu + * if the focused field is within the viewport. + * + * @param sender - The sender of the message + */ + private async triggerSubFrameFocusInRebuild(sender: chrome.runtime.MessageSender) { + this.cancelInlineMenuFadeInAndPositionUpdate(); + this.rebuildSubFrameOffsetsSubject.next(sender); + this.repositionInlineMenuSubject.next(sender); + } + + /** + * Handles determining if the inline menu should be repositioned or closed, and initiates + * the process of calculating the new position of the inline menu. + * + * @param sender - The sender of the message + */ + private repositionInlineMenu = async (sender: chrome.runtime.MessageSender) => { + this.cancelInlineMenuFadeInAndPositionUpdate(); + if (!this.isFieldCurrentlyFocused && !this.isInlineMenuButtonVisible) { + await this.closeInlineMenuAfterReposition(sender); + return; + } + + const isFieldWithinViewport = await BrowserApi.tabSendMessage( + sender.tab, + { command: "checkIsMostRecentlyFocusedFieldWithinViewport" }, + { frameId: this.focusedFieldData.frameId }, + ); + if (!isFieldWithinViewport) { + await this.closeInlineMenuAfterReposition(sender); + return; + } + + if (this.focusedFieldData.frameId > 0) { + this.rebuildSubFrameOffsetsSubject.next(sender); + } + + this.startUpdateInlineMenuPositionSubject.next(sender); + }; + + /** + * Triggers a closure of the inline menu during a reposition event. + * + * @param sender - The sender of the message +| */ + private async closeInlineMenuAfterReposition(sender: chrome.runtime.MessageSender) { + await this.toggleInlineMenuHidden( + { isInlineMenuHidden: false, setTransparentInlineMenu: true }, + sender, + ); + this.closeInlineMenu(sender, { forceCloseInlineMenu: true }); + } + + /** + * Cancels the observables that update the position and fade in of the inline menu. + */ + private cancelInlineMenuFadeInAndPositionUpdate() { + this.cancelInlineMenuFadeIn(); + this.cancelUpdateInlineMenuPositionSubject.next(); + } + /** * Sets up the extension message listeners for the overlay. */ - private setupExtensionMessageListeners() { + private setupExtensionListeners() { BrowserApi.messageListener("overlay.background", this.handleExtensionMessage); + BrowserApi.addListener(chrome.webNavigation.onCommitted, this.handleWebNavigationOnCommitted); BrowserApi.addListener(chrome.runtime.onConnect, this.handlePortOnConnect); } @@ -689,18 +1363,42 @@ class OverlayBackground implements OverlayBackgroundInterface { ) => { const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command]; if (!handler) { - return; + return null; } const messageResponse = handler({ message, sender }); - if (!messageResponse) { + if (typeof messageResponse === "undefined") { + return null; + } + + Promise.resolve(messageResponse) + .then((response) => sendResponse(response)) + .catch(this.logService.error); + return true; + }; + + /** + * Handles clearing page details and sub frame offsets when a frame or tab navigation event occurs. + * + * @param details - The details of the web navigation event + */ + private handleWebNavigationOnCommitted = ( + details: chrome.webNavigation.WebNavigationTransitionCallbackDetails, + ) => { + const { frameId, tabId } = details; + const subFrames = this.subFrameOffsetsForTab[tabId]; + if (frameId === 0) { + this.removePageDetails(tabId); + if (subFrames) { + subFrames.clear(); + delete this.subFrameOffsetsForTab[tabId]; + } return; } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - Promise.resolve(messageResponse).then((response) => sendResponse(response)); - return true; + if (subFrames && subFrames.has(frameId)) { + subFrames.delete(frameId); + } }; /** @@ -709,25 +1407,50 @@ class OverlayBackground implements OverlayBackgroundInterface { * @param port - The port that connected to the extension background */ private handlePortOnConnect = async (port: chrome.runtime.Port) => { - const isOverlayListPort = port.name === AutofillOverlayPort.List; - const isOverlayButtonPort = port.name === AutofillOverlayPort.Button; - if (!isOverlayListPort && !isOverlayButtonPort) { + const isInlineMenuListMessageConnector = port.name === AutofillOverlayPort.ListMessageConnector; + const isInlineMenuButtonMessageConnector = + port.name === AutofillOverlayPort.ButtonMessageConnector; + if (isInlineMenuListMessageConnector || isInlineMenuButtonMessageConnector) { + port.onMessage.addListener(this.handleOverlayElementPortMessage); return; } + const isInlineMenuListPort = port.name === AutofillOverlayPort.List; + const isInlineMenuButtonPort = port.name === AutofillOverlayPort.Button; + if (!isInlineMenuListPort && !isInlineMenuButtonPort) { + return; + } + + if (!this.portKeyForTab[port.sender.tab.id]) { + this.portKeyForTab[port.sender.tab.id] = generateRandomChars(12); + } + this.storeOverlayPort(port); + port.onDisconnect.addListener(this.handlePortOnDisconnect); port.onMessage.addListener(this.handleOverlayElementPortMessage); port.postMessage({ - command: `initAutofillOverlay${isOverlayListPort ? "List" : "Button"}`, + command: `initAutofillInlineMenu${isInlineMenuListPort ? "List" : "Button"}`, + iframeUrl: chrome.runtime.getURL( + `overlay/menu-${isInlineMenuListPort ? "list" : "button"}.html`, + ), + pageTitle: chrome.i18n.getMessage( + isInlineMenuListPort ? "bitwardenVault" : "bitwardenOverlayButton", + ), authStatus: await this.getAuthStatus(), - styleSheetUrl: chrome.runtime.getURL(`overlay/${isOverlayListPort ? "list" : "button"}.css`), + styleSheetUrl: chrome.runtime.getURL( + `overlay/menu-${isInlineMenuListPort ? "list" : "button"}.css`, + ), theme: await firstValueFrom(this.themeStateService.selectedTheme$), - translations: this.getTranslations(), - ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null, + translations: this.getInlineMenuTranslations(), + ciphers: isInlineMenuListPort ? await this.getInlineMenuCipherData() : null, + portKey: this.portKeyForTab[port.sender.tab.id], + portName: isInlineMenuListPort + ? AutofillOverlayPort.ListMessageConnector + : AutofillOverlayPort.ButtonMessageConnector, }); - this.updateOverlayPosition( + void this.updateInlineMenuPosition( { - overlayElement: isOverlayListPort + overlayElement: isInlineMenuListPort ? AutofillOverlayElement.List : AutofillOverlayElement.Button, }, @@ -742,14 +1465,14 @@ class OverlayBackground implements OverlayBackgroundInterface { | */ private storeOverlayPort(port: chrome.runtime.Port) { if (port.name === AutofillOverlayPort.List) { - this.storeExpiredOverlayPort(this.overlayListPort); - this.overlayListPort = port; + this.storeExpiredOverlayPort(this.inlineMenuListPort); + this.inlineMenuListPort = port; return; } if (port.name === AutofillOverlayPort.Button) { - this.storeExpiredOverlayPort(this.overlayButtonPort); - this.overlayButtonPort = port; + this.storeExpiredOverlayPort(this.inlineMenuButtonPort); + this.inlineMenuButtonPort = port; } } @@ -776,15 +1499,20 @@ class OverlayBackground implements OverlayBackgroundInterface { message: OverlayBackgroundExtensionMessage, port: chrome.runtime.Port, ) => { - const command = message?.command; - let handler: CallableFunction | undefined; - - if (port.name === AutofillOverlayPort.Button) { - handler = this.overlayButtonPortMessageHandlers[command]; + const tabPortKey = this.portKeyForTab[port.sender.tab.id]; + if (!tabPortKey || tabPortKey !== message?.portKey) { + return; } - if (port.name === AutofillOverlayPort.List) { - handler = this.overlayListPortMessageHandlers[command]; + const command = message.command; + let handler: CallableFunction | undefined; + + if (port.name === AutofillOverlayPort.ButtonMessageConnector) { + handler = this.inlineMenuButtonPortMessageHandlers[command]; + } + + if (port.name === AutofillOverlayPort.ListMessageConnector) { + handler = this.inlineMenuListPortMessageHandlers[command]; } if (!handler) { @@ -793,6 +1521,22 @@ class OverlayBackground implements OverlayBackgroundInterface { handler({ message, port }); }; -} -export default OverlayBackground; + /** + * Ensures that the inline menu list and button port + * references are reset when they are disconnected. + * + * @param port - The port that was disconnected + */ + private handlePortOnDisconnect = (port: chrome.runtime.Port) => { + if (port.name === AutofillOverlayPort.List) { + this.inlineMenuListPort = null; + this.isInlineMenuListVisible = false; + } + + if (port.name === AutofillOverlayPort.Button) { + this.inlineMenuButtonPort = null; + this.isInlineMenuButtonVisible = false; + } + }; +} diff --git a/apps/browser/src/autofill/background/tabs.background.spec.ts b/apps/browser/src/autofill/background/tabs.background.spec.ts index b95e303f17e..4473eb452f3 100644 --- a/apps/browser/src/autofill/background/tabs.background.spec.ts +++ b/apps/browser/src/autofill/background/tabs.background.spec.ts @@ -11,7 +11,7 @@ import { } from "../spec/testing-utils"; import NotificationBackground from "./notification.background"; -import OverlayBackground from "./overlay.background"; +import { OverlayBackground } from "./overlay.background"; import TabsBackground from "./tabs.background"; describe("TabsBackground", () => { @@ -146,6 +146,7 @@ describe("TabsBackground", () => { beforeEach(() => { mainBackground.onUpdatedRan = false; + mainBackground.configService.getFeatureFlag = jest.fn().mockResolvedValue(true); tabsBackground["focusedWindowId"] = focusedWindowId; tab = mock({ windowId: focusedWindowId, @@ -154,18 +155,6 @@ describe("TabsBackground", () => { }); }); - it("removes the cached page details from the overlay background if the tab status is `loading`", () => { - triggerTabOnUpdatedEvent(focusedWindowId, { status: "loading" }, tab); - - expect(overlayBackground.removePageDetails).toHaveBeenCalledWith(focusedWindowId); - }); - - it("removes the cached page details from the overlay background if the tab status is `unloaded`", () => { - triggerTabOnUpdatedEvent(focusedWindowId, { status: "unloaded" }, tab); - - expect(overlayBackground.removePageDetails).toHaveBeenCalledWith(focusedWindowId); - }); - it("skips updating the current tab data the focusedWindowId is set to a value less than zero", async () => { tab.windowId = -1; triggerTabOnUpdatedEvent(focusedWindowId, { status: "loading" }, tab); diff --git a/apps/browser/src/autofill/background/tabs.background.ts b/apps/browser/src/autofill/background/tabs.background.ts index 53c801ff7bc..f68ae6c6edc 100644 --- a/apps/browser/src/autofill/background/tabs.background.ts +++ b/apps/browser/src/autofill/background/tabs.background.ts @@ -1,7 +1,9 @@ +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; + import MainBackground from "../../background/main.background"; +import { OverlayBackground } from "./abstractions/overlay.background"; import NotificationBackground from "./notification.background"; -import OverlayBackground from "./overlay.background"; export default class TabsBackground { constructor( @@ -86,8 +88,11 @@ export default class TabsBackground { changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab, ) => { + const overlayImprovementsFlag = await this.main.configService.getFeatureFlag( + FeatureFlag.InlineMenuPositioningImprovements, + ); const removePageDetailsStatus = new Set(["loading", "unloaded"]); - if (removePageDetailsStatus.has(changeInfo.status)) { + if (!!overlayImprovementsFlag && removePageDetailsStatus.has(changeInfo.status)) { this.overlayBackground.removePageDetails(tabId); } diff --git a/apps/browser/src/autofill/clipboard/clear-clipboard.ts b/apps/browser/src/autofill/clipboard/clear-clipboard.ts index f8018bb036a..426d6539513 100644 --- a/apps/browser/src/autofill/clipboard/clear-clipboard.ts +++ b/apps/browser/src/autofill/clipboard/clear-clipboard.ts @@ -1,11 +1,9 @@ import { BrowserApi } from "../../platform/browser/browser-api"; -export const clearClipboardAlarmName = "clearClipboard"; - export class ClearClipboard { /** We currently rely on an active tab with an injected content script (`../content/misc-utils.ts`) to clear the clipboard via `window.navigator.clipboard.writeText(text)` - + With https://bugs.chromium.org/p/chromium/issues/detail?id=1160302 it was said that service workers, would have access to the clipboard api and then we could migrate to a simpler solution */ diff --git a/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.spec.ts b/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.spec.ts index 522da229244..d0d42cc06f7 100644 --- a/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.spec.ts +++ b/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.spec.ts @@ -1,30 +1,45 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { firstValueFrom, Subscription } from "rxjs"; import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { setAlarmTime } from "../../platform/alarms/alarm-state"; import { BrowserApi } from "../../platform/browser/browser-api"; +import { BrowserTaskSchedulerService } from "../../platform/services/abstractions/browser-task-scheduler.service"; -import { clearClipboardAlarmName } from "./clear-clipboard"; +import { ClearClipboard } from "./clear-clipboard"; import { GeneratePasswordToClipboardCommand } from "./generate-password-to-clipboard-command"; -jest.mock("../../platform/alarms/alarm-state", () => { +jest.mock("rxjs", () => { + const actual = jest.requireActual("rxjs"); return { - setAlarmTime: jest.fn(), + ...actual, + firstValueFrom: jest.fn(), }; }); -const setAlarmTimeMock = setAlarmTime as jest.Mock; - describe("GeneratePasswordToClipboardCommand", () => { let passwordGenerationService: MockProxy; let autofillSettingsService: MockProxy; + let browserTaskSchedulerService: MockProxy; let sut: GeneratePasswordToClipboardCommand; beforeEach(() => { passwordGenerationService = mock(); + autofillSettingsService = mock(); + browserTaskSchedulerService = mock({ + setTimeout: jest.fn((taskName, timeoutInMs) => { + const timeoutHandle = setTimeout(() => { + if (taskName === ScheduledTaskNames.generatePasswordClearClipboardTimeout) { + void ClearClipboard.run(); + } + }, timeoutInMs); + + return new Subscription(() => clearTimeout(timeoutHandle)); + }), + }); passwordGenerationService.getOptions.mockResolvedValue([{ length: 8 }, {} as any]); @@ -35,6 +50,7 @@ describe("GeneratePasswordToClipboardCommand", () => { sut = new GeneratePasswordToClipboardCommand( passwordGenerationService, autofillSettingsService, + browserTaskSchedulerService, ); }); @@ -44,20 +60,24 @@ describe("GeneratePasswordToClipboardCommand", () => { describe("generatePasswordToClipboard", () => { it("has clear clipboard value", async () => { - jest.spyOn(sut as any, "getClearClipboard").mockImplementation(() => 5 * 60); // 5 minutes + jest.useFakeTimers(); + jest.spyOn(ClearClipboard, "run"); + (firstValueFrom as jest.Mock).mockResolvedValue(2 * 60); // 2 minutes await sut.generatePasswordToClipboard({ id: 1 } as any); + jest.advanceTimersByTime(2 * 60 * 1000); expect(jest.spyOn(BrowserApi, "sendTabsMessage")).toHaveBeenCalledTimes(1); - expect(jest.spyOn(BrowserApi, "sendTabsMessage")).toHaveBeenCalledWith(1, { command: "copyText", text: "PASSWORD", }); - - expect(setAlarmTimeMock).toHaveBeenCalledTimes(1); - - expect(setAlarmTimeMock).toHaveBeenCalledWith(clearClipboardAlarmName, expect.any(Number)); + expect(browserTaskSchedulerService.setTimeout).toHaveBeenCalledTimes(1); + expect(browserTaskSchedulerService.setTimeout).toHaveBeenCalledWith( + ScheduledTaskNames.generatePasswordClearClipboardTimeout, + expect.any(Number), + ); + expect(ClearClipboard.run).toHaveBeenCalledTimes(1); }); it("does not have clear clipboard value", async () => { @@ -71,8 +91,7 @@ describe("GeneratePasswordToClipboardCommand", () => { command: "copyText", text: "PASSWORD", }); - - expect(setAlarmTimeMock).not.toHaveBeenCalled(); + expect(browserTaskSchedulerService.setTimeout).not.toHaveBeenCalled(); }); }); }); diff --git a/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.ts b/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.ts index dadd61fbd12..cf3bc311aea 100644 --- a/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.ts +++ b/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.ts @@ -1,18 +1,25 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, Subscription } from "rxjs"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { setAlarmTime } from "../../platform/alarms/alarm-state"; - -import { clearClipboardAlarmName } from "./clear-clipboard"; +import { ClearClipboard } from "./clear-clipboard"; import { copyToClipboard } from "./copy-to-clipboard-command"; export class GeneratePasswordToClipboardCommand { + private clearClipboardSubscription: Subscription; + constructor( private passwordGenerationService: PasswordGenerationServiceAbstraction, private autofillSettingsService: AutofillSettingsServiceAbstraction, - ) {} + private taskSchedulerService: TaskSchedulerService, + ) { + this.taskSchedulerService.registerTaskHandler( + ScheduledTaskNames.generatePasswordClearClipboardTimeout, + () => ClearClipboard.run(), + ); + } async getClearClipboard() { return await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$); @@ -22,14 +29,18 @@ export class GeneratePasswordToClipboardCommand { const [options] = await this.passwordGenerationService.getOptions(); const password = await this.passwordGenerationService.generatePassword(options); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - copyToClipboard(tab, password); + await copyToClipboard(tab, password); - const clearClipboard = await this.getClearClipboard(); - - if (clearClipboard != null) { - await setAlarmTime(clearClipboardAlarmName, clearClipboard * 1000); + const clearClipboardDelayInSeconds = await this.getClearClipboard(); + if (!clearClipboardDelayInSeconds) { + return; } + + const timeoutInMs = clearClipboardDelayInSeconds * 1000; + this.clearClipboardSubscription?.unsubscribe(); + this.clearClipboardSubscription = this.taskSchedulerService.setTimeout( + ScheduledTaskNames.generatePasswordClearClipboardTimeout, + timeoutInMs, + ); } } diff --git a/apps/browser/src/autofill/content/abstractions/autofill-init.ts b/apps/browser/src/autofill/content/abstractions/autofill-init.ts index 91866ffa0bb..8b00b4ecc9e 100644 --- a/apps/browser/src/autofill/content/abstractions/autofill-init.ts +++ b/apps/browser/src/autofill/content/abstractions/autofill-init.ts @@ -1,46 +1,40 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { AutofillOverlayElementType } from "../../enums/autofill-overlay.enum"; import AutofillScript from "../../models/autofill-script"; -type AutofillExtensionMessage = { +export type AutofillExtensionMessage = { command: string; tab?: chrome.tabs.Tab; sender?: string; fillScript?: AutofillScript; url?: string; + subFrameUrl?: string; + subFrameId?: string; pageDetailsUrl?: string; ciphers?: any; + isInlineMenuHidden?: boolean; + overlayElement?: AutofillOverlayElementType; + isFocusingFieldElement?: boolean; + authStatus?: AuthenticationStatus; + isOpeningFullInlineMenu?: boolean; data?: { - authStatus?: AuthenticationStatus; - isFocusingFieldElement?: boolean; - isOverlayCiphersPopulated?: boolean; - direction?: "previous" | "next"; - isOpeningFullOverlay?: boolean; - forceCloseOverlay?: boolean; - autofillOverlayVisibility?: number; + direction?: "previous" | "next" | "current"; + forceCloseInlineMenu?: boolean; + inlineMenuVisibility?: number; }; }; -type AutofillExtensionMessageParam = { message: AutofillExtensionMessage }; +export type AutofillExtensionMessageParam = { message: AutofillExtensionMessage }; -type AutofillExtensionMessageHandlers = { +export type AutofillExtensionMessageHandlers = { [key: string]: CallableFunction; collectPageDetails: ({ message }: AutofillExtensionMessageParam) => void; collectPageDetailsImmediately: ({ message }: AutofillExtensionMessageParam) => void; fillForm: ({ message }: AutofillExtensionMessageParam) => void; - openAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void; - closeAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void; - addNewVaultItemFromOverlay: () => void; - redirectOverlayFocusOut: ({ message }: AutofillExtensionMessageParam) => void; - updateIsOverlayCiphersPopulated: ({ message }: AutofillExtensionMessageParam) => void; - bgUnlockPopoutOpened: () => void; - bgVaultItemRepromptPopoutOpened: () => void; - updateAutofillOverlayVisibility: ({ message }: AutofillExtensionMessageParam) => void; }; -interface AutofillInit { +export interface AutofillInit { init(): void; destroy(): void; } - -export { AutofillExtensionMessage, AutofillExtensionMessageHandlers, AutofillInit }; diff --git a/apps/browser/src/autofill/content/autofill-init.spec.ts b/apps/browser/src/autofill/content/autofill-init.spec.ts index 302b520e336..e27e8ef73d0 100644 --- a/apps/browser/src/autofill/content/autofill-init.spec.ts +++ b/apps/browser/src/autofill/content/autofill-init.spec.ts @@ -1,26 +1,25 @@ -import { mock } from "jest-mock-extended"; - -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; +import { mock, MockProxy } from "jest-mock-extended"; import AutofillPageDetails from "../models/autofill-page-details"; import AutofillScript from "../models/autofill-script"; -import AutofillOverlayContentService from "../services/autofill-overlay-content.service"; +import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service"; +import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; import { flushPromises, mockQuerySelectorAllDefinedCall, sendMockExtensionMessage, } from "../spec/testing-utils"; -import { RedirectFocusDirection } from "../utils/autofill-overlay.enum"; import { AutofillExtensionMessage } from "./abstractions/autofill-init"; import AutofillInit from "./autofill-init"; describe("AutofillInit", () => { + let inlineMenuElements: MockProxy; + let autofillOverlayContentService: MockProxy; let autofillInit: AutofillInit; - const autofillOverlayContentService = mock(); const originalDocumentReadyState = document.readyState; const mockQuerySelectorAll = mockQuerySelectorAllDefinedCall(); + let sendExtensionMessageSpy: jest.SpyInstance; beforeEach(() => { chrome.runtime.connect = jest.fn().mockReturnValue({ @@ -28,7 +27,12 @@ describe("AutofillInit", () => { addListener: jest.fn(), }, }); - autofillInit = new AutofillInit(autofillOverlayContentService); + inlineMenuElements = mock(); + autofillOverlayContentService = mock(); + autofillInit = new AutofillInit(autofillOverlayContentService, inlineMenuElements); + sendExtensionMessageSpy = jest + .spyOn(autofillInit as any, "sendExtensionMessage") + .mockImplementation(); window.IntersectionObserver = jest.fn(() => mock()); }); @@ -61,13 +65,9 @@ describe("AutofillInit", () => { autofillInit.init(); jest.advanceTimersByTime(250); - expect(chrome.runtime.sendMessage).toHaveBeenCalledWith( - { - command: "bgCollectPageDetails", - sender: "autofillInit", - }, - expect.any(Function), - ); + expect(sendExtensionMessageSpy).toHaveBeenCalledWith("bgCollectPageDetails", { + sender: "autofillInit", + }); }); it("registers a window load listener to collect the page details if the document is not in a `complete` ready state", () => { @@ -106,15 +106,15 @@ describe("AutofillInit", () => { sender = mock(); }); - it("returns a undefined value if a extension message handler is not found with the given message command", () => { + it("returns a null value if a extension message handler is not found with the given message command", () => { message.command = "unknownCommand"; const response = autofillInit["handleExtensionMessage"](message, sender, sendResponse); - expect(response).toBe(undefined); + expect(response).toBe(null); }); - it("returns a undefined value if the message handler does not return a response", async () => { + it("returns a null value if the message handler does not return a response", async () => { const response1 = await autofillInit["handleExtensionMessage"](message, sender, sendResponse); await flushPromises(); @@ -126,7 +126,7 @@ describe("AutofillInit", () => { const response2 = autofillInit["handleExtensionMessage"](message, sender, sendResponse); await flushPromises(); - expect(response2).toBe(undefined); + expect(response2).toBe(null); }); it("returns a true value and calls sendResponse if the message handler returns a response", async () => { @@ -155,6 +155,22 @@ describe("AutofillInit", () => { autofillInit.init(); }); + it("triggers extension message handlers from the AutofillOverlayContentService", () => { + autofillOverlayContentService.messageHandlers.messageHandler = jest.fn(); + + sendMockExtensionMessage({ command: "messageHandler" }, sender, sendResponse); + + expect(autofillOverlayContentService.messageHandlers.messageHandler).toHaveBeenCalled(); + }); + + it("triggers extension message handlers from the AutofillInlineMenuContentService", () => { + inlineMenuElements.messageHandlers.messageHandler = jest.fn(); + + sendMockExtensionMessage({ command: "messageHandler" }, sender, sendResponse); + + expect(inlineMenuElements.messageHandlers.messageHandler).toHaveBeenCalled(); + }); + describe("collectPageDetails", () => { it("sends the collected page details for autofill using a background script message", async () => { const pageDetails: AutofillPageDetails = { @@ -177,8 +193,7 @@ describe("AutofillInit", () => { sendMockExtensionMessage(message, sender, sendResponse); await flushPromises(); - expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ - command: "collectPageDetailsResponse", + expect(sendExtensionMessageSpy).toHaveBeenCalledWith("collectPageDetailsResponse", { tab: message.tab, details: pageDetails, sender: message.sender, @@ -226,14 +241,11 @@ describe("AutofillInit", () => { }); it("skips calling the InsertAutofillContentService and does not fill the form if the url to fill is not equal to the current tab url", async () => { - const fillScript = mock(); - const message = { + sendMockExtensionMessage({ command: "fillForm", fillScript, pageDetailsUrl: "https://a-different-url.com", - }; - - sendMockExtensionMessage(message); + }); await flushPromises(); expect(autofillInit["insertAutofillContentService"].fillForm).not.toHaveBeenCalledWith( @@ -255,7 +267,10 @@ describe("AutofillInit", () => { }); it("removes the overlay when filling the form", async () => { - const blurAndRemoveOverlaySpy = jest.spyOn(autofillInit as any, "blurAndRemoveOverlay"); + const blurAndRemoveOverlaySpy = jest.spyOn( + autofillInit as any, + "blurFocusedFieldAndCloseInlineMenu", + ); sendMockExtensionMessage({ command: "fillForm", fillScript, @@ -268,10 +283,6 @@ describe("AutofillInit", () => { it("updates the isCurrentlyFilling property of the overlay to true after filling", async () => { jest.useFakeTimers(); - jest.spyOn(autofillInit as any, "updateOverlayIsCurrentlyFilling"); - jest - .spyOn(autofillInit["autofillOverlayContentService"], "focusMostRecentOverlayField") - .mockImplementation(); sendMockExtensionMessage({ command: "fillForm", @@ -281,292 +292,18 @@ describe("AutofillInit", () => { await flushPromises(); jest.advanceTimersByTime(300); - expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(1, true); + expect(sendExtensionMessageSpy).toHaveBeenNthCalledWith( + 1, + "updateIsFieldCurrentlyFilling", + { isFieldCurrentlyFilling: true }, + ); expect(autofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( fillScript, ); - expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(2, false); - }); - - it("skips attempting to focus the most recent field if the autofillOverlayContentService is not present", async () => { - jest.useFakeTimers(); - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "updateOverlayIsCurrentlyFilling"); - jest - .spyOn(newAutofillInit["insertAutofillContentService"], "fillForm") - .mockImplementation(); - - sendMockExtensionMessage({ - command: "fillForm", - fillScript, - pageDetailsUrl: window.location.href, - }); - await flushPromises(); - jest.advanceTimersByTime(300); - - expect(newAutofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith( - 1, - true, - ); - expect(newAutofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( - fillScript, - ); - expect(newAutofillInit["updateOverlayIsCurrentlyFilling"]).not.toHaveBeenNthCalledWith( + expect(sendExtensionMessageSpy).toHaveBeenNthCalledWith( 2, - false, - ); - }); - }); - - describe("openAutofillOverlay", () => { - const message = { - command: "openAutofillOverlay", - data: { - isFocusingFieldElement: true, - isOpeningFullOverlay: true, - authStatus: AuthenticationStatus.Unlocked, - }, - }; - - it("skips attempting to open the autofill overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage(message); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("opens the autofill overlay", () => { - sendMockExtensionMessage(message); - - expect( - autofillInit["autofillOverlayContentService"].openAutofillOverlay, - ).toHaveBeenCalledWith({ - isFocusingFieldElement: message.data.isFocusingFieldElement, - isOpeningFullOverlay: message.data.isOpeningFullOverlay, - authStatus: message.data.authStatus, - }); - }); - }); - - describe("closeAutofillOverlay", () => { - beforeEach(() => { - autofillInit["autofillOverlayContentService"].isFieldCurrentlyFocused = false; - autofillInit["autofillOverlayContentService"].isCurrentlyFilling = false; - }); - - it("skips attempting to remove the overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ - command: "closeAutofillOverlay", - data: { forceCloseOverlay: false }, - }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("removes the autofill overlay if the message flags a forced closure", () => { - sendMockExtensionMessage({ - command: "closeAutofillOverlay", - data: { forceCloseOverlay: true }, - }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).toHaveBeenCalled(); - }); - - it("ignores the message if a field is currently focused", () => { - autofillInit["autofillOverlayContentService"].isFieldCurrentlyFocused = true; - - sendMockExtensionMessage({ command: "closeAutofillOverlay" }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, - ).not.toHaveBeenCalled(); - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).not.toHaveBeenCalled(); - }); - - it("removes the autofill overlay list if the overlay is currently filling", () => { - autofillInit["autofillOverlayContentService"].isCurrentlyFilling = true; - - sendMockExtensionMessage({ command: "closeAutofillOverlay" }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, - ).toHaveBeenCalled(); - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).not.toHaveBeenCalled(); - }); - - it("removes the entire overlay if the overlay is not currently filling", () => { - sendMockExtensionMessage({ command: "closeAutofillOverlay" }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, - ).not.toHaveBeenCalled(); - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).toHaveBeenCalled(); - }); - }); - - describe("addNewVaultItemFromOverlay", () => { - it("will not add a new vault item if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("will add a new vault item", () => { - sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" }); - - expect(autofillInit["autofillOverlayContentService"].addNewVaultItem).toHaveBeenCalled(); - }); - }); - - describe("redirectOverlayFocusOut", () => { - const message = { - command: "redirectOverlayFocusOut", - data: { - direction: RedirectFocusDirection.Next, - }, - }; - - it("ignores the message to redirect focus if the autofillOverlayContentService does not exist", () => { - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage(message); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("redirects the overlay focus", () => { - sendMockExtensionMessage(message); - - expect( - autofillInit["autofillOverlayContentService"].redirectOverlayFocusOut, - ).toHaveBeenCalledWith(message.data.direction); - }); - }); - - describe("updateIsOverlayCiphersPopulated", () => { - const message = { - command: "updateIsOverlayCiphersPopulated", - data: { - isOverlayCiphersPopulated: true, - }, - }; - - it("skips updating whether the ciphers are populated if the autofillOverlayContentService does note exist", () => { - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage(message); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("updates whether the overlay ciphers are populated", () => { - sendMockExtensionMessage(message); - - expect(autofillInit["autofillOverlayContentService"].isOverlayCiphersPopulated).toEqual( - message.data.isOverlayCiphersPopulated, - ); - }); - }); - - describe("bgUnlockPopoutOpened", () => { - it("skips attempting to blur and remove the overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgUnlockPopoutOpened" }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - expect(newAutofillInit["removeAutofillOverlay"]).not.toHaveBeenCalled(); - }); - - it("blurs the most recently focused feel and remove the autofill overlay", () => { - jest.spyOn(autofillInit["autofillOverlayContentService"], "blurMostRecentOverlayField"); - jest.spyOn(autofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgUnlockPopoutOpened" }); - - expect( - autofillInit["autofillOverlayContentService"].blurMostRecentOverlayField, - ).toHaveBeenCalled(); - expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled(); - }); - }); - - describe("bgVaultItemRepromptPopoutOpened", () => { - it("skips attempting to blur and remove the overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgVaultItemRepromptPopoutOpened" }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - expect(newAutofillInit["removeAutofillOverlay"]).not.toHaveBeenCalled(); - }); - - it("blurs the most recently focused feel and remove the autofill overlay", () => { - jest.spyOn(autofillInit["autofillOverlayContentService"], "blurMostRecentOverlayField"); - jest.spyOn(autofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgVaultItemRepromptPopoutOpened" }); - - expect( - autofillInit["autofillOverlayContentService"].blurMostRecentOverlayField, - ).toHaveBeenCalled(); - expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled(); - }); - }); - - describe("updateAutofillOverlayVisibility", () => { - beforeEach(() => { - autofillInit["autofillOverlayContentService"].autofillOverlayVisibility = - AutofillOverlayVisibility.OnButtonClick; - }); - - it("skips attempting to update the overlay visibility if the autofillOverlayVisibility data value is not present", () => { - sendMockExtensionMessage({ - command: "updateAutofillOverlayVisibility", - data: {}, - }); - - expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual( - AutofillOverlayVisibility.OnButtonClick, - ); - }); - - it("updates the overlay visibility value", () => { - const message = { - command: "updateAutofillOverlayVisibility", - data: { - autofillOverlayVisibility: AutofillOverlayVisibility.Off, - }, - }; - - sendMockExtensionMessage(message); - - expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual( - message.data.autofillOverlayVisibility, + "updateIsFieldCurrentlyFilling", + { isFieldCurrentlyFilling: false }, ); }); }); diff --git a/apps/browser/src/autofill/content/autofill-init.ts b/apps/browser/src/autofill/content/autofill-init.ts index e78a1fb5ee1..70f815d2234 100644 --- a/apps/browser/src/autofill/content/autofill-init.ts +++ b/apps/browser/src/autofill/content/autofill-init.ts @@ -1,4 +1,7 @@ +import { EVENTS } from "@bitwarden/common/autofill/constants"; + import AutofillPageDetails from "../models/autofill-page-details"; +import { AutofillInlineMenuContentService } from "../overlay/inline-menu/abstractions/autofill-inline-menu-content.service"; import { AutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service"; import CollectAutofillContentService from "../services/collect-autofill-content.service"; import DomElementVisibilityService from "../services/dom-element-visibility.service"; @@ -12,7 +15,9 @@ import { } from "./abstractions/autofill-init"; class AutofillInit implements AutofillInitInterface { + private readonly sendExtensionMessage = sendExtensionMessage; private readonly autofillOverlayContentService: AutofillOverlayContentService | undefined; + private readonly autofillInlineMenuContentService: AutofillInlineMenuContentService | undefined; private readonly domElementVisibilityService: DomElementVisibilityService; private readonly collectAutofillContentService: CollectAutofillContentService; private readonly insertAutofillContentService: InsertAutofillContentService; @@ -21,14 +26,6 @@ class AutofillInit implements AutofillInitInterface { collectPageDetails: ({ message }) => this.collectPageDetails(message), collectPageDetailsImmediately: ({ message }) => this.collectPageDetails(message, true), fillForm: ({ message }) => this.fillForm(message), - openAutofillOverlay: ({ message }) => this.openAutofillOverlay(message), - closeAutofillOverlay: ({ message }) => this.removeAutofillOverlay(message), - addNewVaultItemFromOverlay: () => this.addNewVaultItemFromOverlay(), - redirectOverlayFocusOut: ({ message }) => this.redirectOverlayFocusOut(message), - updateIsOverlayCiphersPopulated: ({ message }) => this.updateIsOverlayCiphersPopulated(message), - bgUnlockPopoutOpened: () => this.blurAndRemoveOverlay(), - bgVaultItemRepromptPopoutOpened: () => this.blurAndRemoveOverlay(), - updateAutofillOverlayVisibility: ({ message }) => this.updateAutofillOverlayVisibility(message), }; /** @@ -36,10 +33,17 @@ class AutofillInit implements AutofillInitInterface { * CollectAutofillContentService and InsertAutofillContentService classes. * * @param autofillOverlayContentService - The autofill overlay content service, potentially undefined. + * @param inlineMenuElements - The inline menu elements, potentially undefined. */ - constructor(autofillOverlayContentService?: AutofillOverlayContentService) { + constructor( + autofillOverlayContentService?: AutofillOverlayContentService, + inlineMenuElements?: AutofillInlineMenuContentService, + ) { this.autofillOverlayContentService = autofillOverlayContentService; - this.domElementVisibilityService = new DomElementVisibilityService(); + this.autofillInlineMenuContentService = inlineMenuElements; + this.domElementVisibilityService = new DomElementVisibilityService( + this.autofillInlineMenuContentService, + ); this.collectAutofillContentService = new CollectAutofillContentService( this.domElementVisibilityService, this.autofillOverlayContentService, @@ -70,7 +74,7 @@ class AutofillInit implements AutofillInitInterface { const sendCollectDetailsMessage = () => { this.clearCollectPageDetailsOnLoadTimeout(); this.collectPageDetailsOnLoadTimeout = setTimeout( - () => sendExtensionMessage("bgCollectPageDetails", { sender: "autofillInit" }), + () => this.sendExtensionMessage("bgCollectPageDetails", { sender: "autofillInit" }), 250, ); }; @@ -79,7 +83,7 @@ class AutofillInit implements AutofillInitInterface { sendCollectDetailsMessage(); } - globalThis.addEventListener("load", sendCollectDetailsMessage); + globalThis.addEventListener(EVENTS.LOAD, sendCollectDetailsMessage); } /** @@ -102,8 +106,7 @@ class AutofillInit implements AutofillInitInterface { return pageDetails; } - void chrome.runtime.sendMessage({ - command: "collectPageDetailsResponse", + void this.sendExtensionMessage("collectPageDetailsResponse", { tab: message.tab, details: pageDetails, sender: message.sender, @@ -120,134 +123,28 @@ class AutofillInit implements AutofillInitInterface { return; } - this.blurAndRemoveOverlay(); - this.updateOverlayIsCurrentlyFilling(true); + this.blurFocusedFieldAndCloseInlineMenu(); + await this.sendExtensionMessage("updateIsFieldCurrentlyFilling", { + isFieldCurrentlyFilling: true, + }); await this.insertAutofillContentService.fillForm(fillScript); - if (!this.autofillOverlayContentService) { - return; - } - - setTimeout(() => this.updateOverlayIsCurrentlyFilling(false), 250); - } - - /** - * Handles updating the overlay is currently filling value. - * - * @param isCurrentlyFilling - Indicates if the overlay is currently filling - */ - private updateOverlayIsCurrentlyFilling(isCurrentlyFilling: boolean) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.isCurrentlyFilling = isCurrentlyFilling; - } - - /** - * Opens the autofill overlay. - * - * @param data - The extension message data. - */ - private openAutofillOverlay({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.openAutofillOverlay(data); - } - - /** - * Blurs the most recent overlay field and removes the overlay. Used - * in cases where the background unlock or vault item reprompt popout - * is opened. - */ - private blurAndRemoveOverlay() { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.blurMostRecentOverlayField(); - this.removeAutofillOverlay(); - } - - /** - * Removes the autofill overlay if the field is not currently focused. - * If the autofill is currently filling, only the overlay list will be - * removed. - */ - private removeAutofillOverlay(message?: AutofillExtensionMessage) { - if (message?.data?.forceCloseOverlay) { - this.autofillOverlayContentService?.removeAutofillOverlay(); - return; - } - - if ( - !this.autofillOverlayContentService || - this.autofillOverlayContentService.isFieldCurrentlyFocused - ) { - return; - } - - if (this.autofillOverlayContentService.isCurrentlyFilling) { - this.autofillOverlayContentService.removeAutofillOverlayList(); - return; - } - - this.autofillOverlayContentService.removeAutofillOverlay(); - } - - /** - * Adds a new vault item from the overlay. - */ - private addNewVaultItemFromOverlay() { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.addNewVaultItem(); - } - - /** - * Redirects the overlay focus out of an overlay iframe. - * - * @param data - Contains the direction to redirect the focus. - */ - private redirectOverlayFocusOut({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.redirectOverlayFocusOut(data?.direction); - } - - /** - * Updates whether the current tab has ciphers that can populate the overlay list - * - * @param data - Contains the isOverlayCiphersPopulated value - * - */ - private updateIsOverlayCiphersPopulated({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.isOverlayCiphersPopulated = Boolean( - data?.isOverlayCiphersPopulated, + setTimeout( + () => + this.sendExtensionMessage("updateIsFieldCurrentlyFilling", { + isFieldCurrentlyFilling: false, + }), + 250, ); } /** - * Updates the autofill overlay visibility. - * - * @param data - Contains the autoFillOverlayVisibility value + * Blurs the most recently focused field and removes the inline menu. Used + * in cases where the background unlock or vault item reprompt popout + * is opened. */ - private updateAutofillOverlayVisibility({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService || isNaN(data?.autofillOverlayVisibility)) { - return; - } - - this.autofillOverlayContentService.autofillOverlayVisibility = data?.autofillOverlayVisibility; + private blurFocusedFieldAndCloseInlineMenu() { + this.autofillOverlayContentService?.blurMostRecentlyFocusedField(true); } /** @@ -279,22 +176,37 @@ class AutofillInit implements AutofillInitInterface { sendResponse: (response?: any) => void, ): boolean => { const command: string = message.command; - const handler: CallableFunction | undefined = this.extensionMessageHandlers[command]; + const handler: CallableFunction | undefined = this.getExtensionMessageHandler(command); if (!handler) { - return; + return null; } const messageResponse = handler({ message, sender }); - if (!messageResponse) { - return; + if (typeof messageResponse === "undefined") { + return null; } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - Promise.resolve(messageResponse).then((response) => sendResponse(response)); + void Promise.resolve(messageResponse).then((response) => sendResponse(response)); return true; }; + /** + * Gets the extension message handler for the given command. + * + * @param command - The extension message command. + */ + private getExtensionMessageHandler(command: string): CallableFunction | undefined { + if (this.autofillOverlayContentService?.messageHandlers?.[command]) { + return this.autofillOverlayContentService.messageHandlers[command]; + } + + if (this.autofillInlineMenuContentService?.messageHandlers?.[command]) { + return this.autofillInlineMenuContentService.messageHandlers[command]; + } + + return this.extensionMessageHandlers[command]; + } + /** * Handles destroying the autofill init content script. Removes all * listeners, timeouts, and object instances to prevent memory leaks. @@ -304,6 +216,7 @@ class AutofillInit implements AutofillInitInterface { chrome.runtime.onMessage.removeListener(this.handleExtensionMessage); this.collectAutofillContentService.destroy(); this.autofillOverlayContentService?.destroy(); + this.autofillInlineMenuContentService?.destroy(); } } diff --git a/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts b/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts index ab21e367c29..22430227660 100644 --- a/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts +++ b/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts @@ -1,12 +1,24 @@ -import AutofillOverlayContentService from "../services/autofill-overlay-content.service"; +import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service"; +import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; +import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service"; import { setupAutofillInitDisconnectAction } from "../utils"; import AutofillInit from "./autofill-init"; (function (windowContext) { if (!windowContext.bitwardenAutofillInit) { - const autofillOverlayContentService = new AutofillOverlayContentService(); - windowContext.bitwardenAutofillInit = new AutofillInit(autofillOverlayContentService); + const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); + const autofillOverlayContentService = new AutofillOverlayContentService( + inlineMenuFieldQualificationService, + ); + let inlineMenuElements: AutofillInlineMenuContentService; + if (globalThis.self === globalThis.top) { + inlineMenuElements = new AutofillInlineMenuContentService(); + } + windowContext.bitwardenAutofillInit = new AutofillInit( + autofillOverlayContentService, + inlineMenuElements, + ); setupAutofillInitDisconnectAction(windowContext); windowContext.bitwardenAutofillInit.init(); diff --git a/apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts b/apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts new file mode 100644 index 00000000000..88b78dc2495 --- /dev/null +++ b/apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts @@ -0,0 +1,124 @@ +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; + +import { LockedVaultPendingNotificationsData } from "../../../background/abstractions/notification.background"; +import AutofillPageDetails from "../../../models/autofill-page-details"; + +type WebsiteIconData = { + imageEnabled: boolean; + image: string; + fallbackImage: string; + icon: string; +}; + +type OverlayAddNewItemMessage = { + login?: { + uri?: string; + hostname: string; + username: string; + password: string; + }; +}; + +type OverlayBackgroundExtensionMessage = { + [key: string]: any; + command: string; + tab?: chrome.tabs.Tab; + sender?: string; + details?: AutofillPageDetails; + overlayElement?: string; + display?: string; + data?: LockedVaultPendingNotificationsData; +} & OverlayAddNewItemMessage; + +type OverlayPortMessage = { + [key: string]: any; + command: string; + direction?: string; + overlayCipherId?: string; +}; + +type FocusedFieldData = { + focusedFieldStyles: Partial; + focusedFieldRects: Partial; + tabId?: number; +}; + +type OverlayCipherData = { + id: string; + name: string; + type: CipherType; + reprompt: CipherRepromptType; + favorite: boolean; + icon: { imageEnabled: boolean; image: string; fallbackImage: string; icon: string }; + login?: { username: string }; + card?: string; +}; + +type BackgroundMessageParam = { + message: OverlayBackgroundExtensionMessage; +}; +type BackgroundSenderParam = { + sender: chrome.runtime.MessageSender; +}; +type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam; + +type OverlayBackgroundExtensionMessageHandlers = { + [key: string]: CallableFunction; + openAutofillOverlay: () => void; + autofillOverlayElementClosed: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + autofillOverlayAddNewVaultItem: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + getAutofillOverlayVisibility: () => void; + checkAutofillOverlayFocused: () => void; + focusAutofillOverlayList: () => void; + updateAutofillOverlayPosition: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + updateAutofillOverlayHidden: ({ message }: BackgroundMessageParam) => void; + updateFocusedFieldData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + unlockCompleted: ({ message }: BackgroundMessageParam) => void; + addedCipher: () => void; + addEditCipherSubmitted: () => void; + editedCipher: () => void; + deletedCipher: () => void; +}; + +type PortMessageParam = { + message: OverlayPortMessage; +}; +type PortConnectionParam = { + port: chrome.runtime.Port; +}; +type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam; + +type OverlayButtonPortMessageHandlers = { + [key: string]: CallableFunction; + overlayButtonClicked: ({ port }: PortConnectionParam) => void; + closeAutofillOverlay: ({ port }: PortConnectionParam) => void; + forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void; + overlayPageBlurred: () => void; + redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; +}; + +type OverlayListPortMessageHandlers = { + [key: string]: CallableFunction; + checkAutofillOverlayButtonFocused: () => void; + forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void; + overlayPageBlurred: () => void; + unlockVault: ({ port }: PortConnectionParam) => void; + fillSelectedListItem: ({ message, port }: PortOnMessageHandlerParams) => void; + addNewVaultItem: ({ port }: PortConnectionParam) => void; + viewSelectedCipher: ({ message, port }: PortOnMessageHandlerParams) => void; + redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; +}; + +export { + WebsiteIconData, + OverlayBackgroundExtensionMessage, + OverlayPortMessage, + FocusedFieldData, + OverlayCipherData, + OverlayAddNewItemMessage, + OverlayBackgroundExtensionMessageHandlers, + OverlayButtonPortMessageHandlers, + OverlayListPortMessageHandlers, +}; diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts new file mode 100644 index 00000000000..c3285059c7e --- /dev/null +++ b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts @@ -0,0 +1,1463 @@ +import { mock, MockProxy, mockReset } from "jest-mock-extended"; +import { BehaviorSubject, of } from "rxjs"; + +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { AuthService } from "@bitwarden/common/auth/services/auth.service"; +import { + SHOW_AUTOFILL_BUTTON, + AutofillOverlayVisibility, +} from "@bitwarden/common/autofill/constants"; +import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { + DefaultDomainSettingsService, + DomainSettingsService, +} from "@bitwarden/common/autofill/services/domain-settings.service"; +import { + EnvironmentService, + Region, +} from "@bitwarden/common/platform/abstractions/environment.service"; +import { ThemeType } from "@bitwarden/common/platform/enums"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { CloudEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; +import { I18nService } from "@bitwarden/common/platform/services/i18n.service"; +import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { + FakeStateProvider, + FakeAccountService, + mockAccountServiceWith, +} from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; + +import { BrowserApi } from "../../../platform/browser/browser-api"; +import { BrowserPlatformUtilsService } from "../../../platform/services/platform-utils/browser-platform-utils.service"; +import { + AutofillOverlayElement, + AutofillOverlayPort, + RedirectFocusDirection, +} from "../../enums/autofill-overlay.enum"; +import { AutofillService } from "../../services/abstractions/autofill.service"; +import { + createAutofillPageDetailsMock, + createChromeTabMock, + createFocusedFieldDataMock, + createPageDetailMock, + createPortSpyMock, +} from "../../spec/autofill-mocks"; +import { flushPromises, sendMockExtensionMessage, sendPortMessage } from "../../spec/testing-utils"; + +import LegacyOverlayBackground from "./overlay.background.deprecated"; + +describe("OverlayBackground", () => { + const mockUserId = Utils.newGuid() as UserId; + const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); + const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService); + let domainSettingsService: DomainSettingsService; + let buttonPortSpy: chrome.runtime.Port; + let listPortSpy: chrome.runtime.Port; + let overlayBackground: LegacyOverlayBackground; + const cipherService = mock(); + const autofillService = mock(); + let activeAccountStatusMock$: BehaviorSubject; + let authService: MockProxy; + + const environmentService = mock(); + environmentService.environment$ = new BehaviorSubject( + new CloudEnvironment({ + key: Region.US, + domain: "bitwarden.com", + urls: { icons: "https://icons.bitwarden.com/" }, + }), + ); + const autofillSettingsService = mock(); + const i18nService = mock(); + const platformUtilsService = mock(); + const themeStateService = mock(); + const initOverlayElementPorts = async (options = { initList: true, initButton: true }) => { + const { initList, initButton } = options; + if (initButton) { + await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.Button)); + buttonPortSpy = overlayBackground["overlayButtonPort"]; + } + + if (initList) { + await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.List)); + listPortSpy = overlayBackground["overlayListPort"]; + } + + return { buttonPortSpy, listPortSpy }; + }; + + beforeEach(() => { + domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); + activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked); + authService = mock(); + authService.activeAccountStatus$ = activeAccountStatusMock$; + overlayBackground = new LegacyOverlayBackground( + cipherService, + autofillService, + authService, + environmentService, + domainSettingsService, + autofillSettingsService, + i18nService, + platformUtilsService, + themeStateService, + ); + + jest + .spyOn(overlayBackground as any, "getOverlayVisibility") + .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); + + themeStateService.selectedTheme$ = of(ThemeType.Light); + domainSettingsService.showFavicons$ = of(true); + + void overlayBackground.init(); + }); + + afterEach(() => { + jest.clearAllMocks(); + mockReset(cipherService); + }); + + describe("removePageDetails", () => { + it("removes the page details for a specific tab from the pageDetailsForTab object", () => { + const tabId = 1; + const frameId = 2; + overlayBackground["pageDetailsForTab"][tabId] = new Map([[frameId, createPageDetailMock()]]); + overlayBackground.removePageDetails(tabId); + + expect(overlayBackground["pageDetailsForTab"][tabId]).toBeUndefined(); + }); + }); + + describe("init", () => { + it("sets up the extension message listeners, get the overlay's visibility settings, and get the user's auth status", async () => { + overlayBackground["setupExtensionMessageListeners"] = jest.fn(); + overlayBackground["getOverlayVisibility"] = jest.fn(); + overlayBackground["getAuthStatus"] = jest.fn(); + + await overlayBackground.init(); + + expect(overlayBackground["setupExtensionMessageListeners"]).toHaveBeenCalled(); + expect(overlayBackground["getOverlayVisibility"]).toHaveBeenCalled(); + expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); + }); + }); + + describe("updateOverlayCiphers", () => { + const url = "https://jest-testing-website.com"; + const tab = createChromeTabMock({ url }); + const cipher1 = mock({ + id: "id-1", + localData: { lastUsedDate: 222 }, + name: "name-1", + type: CipherType.Login, + login: { username: "username-1", uri: url }, + }); + const cipher2 = mock({ + id: "id-2", + localData: { lastUsedDate: 111 }, + name: "name-2", + type: CipherType.Login, + login: { username: "username-2", uri: url }, + }); + + beforeEach(() => { + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + }); + + it("ignores updating the overlay ciphers if the user's auth status is not unlocked", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Locked); + jest.spyOn(BrowserApi, "getTabFromCurrentWindowId"); + jest.spyOn(cipherService, "getAllDecryptedForUrl"); + + await overlayBackground.updateOverlayCiphers(); + + expect(BrowserApi.getTabFromCurrentWindowId).not.toHaveBeenCalled(); + expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled(); + }); + + it("ignores updating the overlay ciphers if the tab is undefined", async () => { + jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(undefined); + jest.spyOn(cipherService, "getAllDecryptedForUrl"); + + await overlayBackground.updateOverlayCiphers(); + + expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); + expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled(); + }); + + it("queries all ciphers for the given url, sort them by last used, and format them for usage in the overlay", async () => { + jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(tab); + cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); + cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); + jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); + jest.spyOn(overlayBackground as any, "getOverlayCipherData"); + + await overlayBackground.updateOverlayCiphers(); + + expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); + expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url); + expect(overlayBackground["cipherService"].sortCiphersByLastUsedThenName).toHaveBeenCalled(); + expect(overlayBackground["overlayLoginCiphers"]).toStrictEqual( + new Map([ + ["overlay-cipher-0", cipher2], + ["overlay-cipher-1", cipher1], + ]), + ); + expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled(); + }); + + it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateIsOverlayCiphersPopulated` message to the tab indicating that the list of ciphers is populated", async () => { + overlayBackground["overlayListPort"] = mock(); + cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); + cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); + jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(tab); + jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); + + await overlayBackground.updateOverlayCiphers(); + + expect(overlayBackground["overlayListPort"].postMessage).toHaveBeenCalledWith({ + command: "updateOverlayListCiphers", + ciphers: [ + { + card: null, + favorite: cipher2.favorite, + icon: { + fallbackImage: "images/bwi-globe.png", + icon: "bwi-globe", + image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", + imageEnabled: true, + }, + id: "overlay-cipher-0", + login: { + username: "username-2", + }, + name: "name-2", + reprompt: cipher2.reprompt, + type: 1, + }, + { + card: null, + favorite: cipher1.favorite, + icon: { + fallbackImage: "images/bwi-globe.png", + icon: "bwi-globe", + image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", + imageEnabled: true, + }, + id: "overlay-cipher-1", + login: { + username: "username-1", + }, + name: "name-1", + reprompt: cipher1.reprompt, + type: 1, + }, + ], + }); + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + tab, + "updateIsOverlayCiphersPopulated", + { isOverlayCiphersPopulated: true }, + ); + }); + }); + + describe("getOverlayCipherData", () => { + const url = "https://jest-testing-website.com"; + const cipher1 = mock({ + id: "id-1", + localData: { lastUsedDate: 222 }, + name: "name-1", + type: CipherType.Login, + login: { username: "username-1", uri: url }, + }); + const cipher2 = mock({ + id: "id-2", + localData: { lastUsedDate: 111 }, + name: "name-2", + type: CipherType.Login, + login: { username: "username-2", uri: url }, + }); + const cipher3 = mock({ + id: "id-3", + localData: { lastUsedDate: 333 }, + name: "name-3", + type: CipherType.Card, + card: { subTitle: "Visa, *6789" }, + }); + const cipher4 = mock({ + id: "id-4", + localData: { lastUsedDate: 444 }, + name: "name-4", + type: CipherType.Card, + card: { subTitle: "Mastercard, *1234" }, + }); + + it("formats and returns the cipher data", async () => { + overlayBackground["overlayLoginCiphers"] = new Map([ + ["overlay-cipher-0", cipher2], + ["overlay-cipher-1", cipher1], + ["overlay-cipher-2", cipher3], + ["overlay-cipher-3", cipher4], + ]); + + const overlayCipherData = await overlayBackground["getOverlayCipherData"](); + + expect(overlayCipherData).toStrictEqual([ + { + card: null, + favorite: cipher2.favorite, + icon: { + fallbackImage: "images/bwi-globe.png", + icon: "bwi-globe", + image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", + imageEnabled: true, + }, + id: "overlay-cipher-0", + login: { + username: "username-2", + }, + name: "name-2", + reprompt: cipher2.reprompt, + type: 1, + }, + { + card: null, + favorite: cipher1.favorite, + icon: { + fallbackImage: "images/bwi-globe.png", + icon: "bwi-globe", + image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", + imageEnabled: true, + }, + id: "overlay-cipher-1", + login: { + username: "username-1", + }, + name: "name-1", + reprompt: cipher1.reprompt, + type: 1, + }, + { + card: "Visa, *6789", + favorite: cipher3.favorite, + icon: { + fallbackImage: "", + icon: "bwi-credit-card", + image: undefined, + imageEnabled: true, + }, + id: "overlay-cipher-2", + login: null, + name: "name-3", + reprompt: cipher3.reprompt, + type: 3, + }, + { + card: "Mastercard, *1234", + favorite: cipher4.favorite, + icon: { + fallbackImage: "", + icon: "bwi-credit-card", + image: undefined, + imageEnabled: true, + }, + id: "overlay-cipher-3", + login: null, + name: "name-4", + reprompt: cipher4.reprompt, + type: 3, + }, + ]); + }); + }); + + describe("getAuthStatus", () => { + it("will update the user's auth status but will not update the overlay ciphers", async () => { + const authStatus = AuthenticationStatus.Unlocked; + overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; + jest.spyOn(overlayBackground["authService"], "getAuthStatus").mockResolvedValue(authStatus); + jest.spyOn(overlayBackground as any, "updateOverlayButtonAuthStatus").mockImplementation(); + jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation(); + + const status = await overlayBackground["getAuthStatus"](); + + expect(overlayBackground["authService"].getAuthStatus).toHaveBeenCalled(); + expect(overlayBackground["updateOverlayButtonAuthStatus"]).not.toHaveBeenCalled(); + expect(overlayBackground["updateOverlayCiphers"]).not.toHaveBeenCalled(); + expect(overlayBackground["userAuthStatus"]).toBe(authStatus); + expect(status).toBe(authStatus); + }); + + it("will update the user's auth status and update the overlay ciphers if the status has been modified", async () => { + const authStatus = AuthenticationStatus.Unlocked; + overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; + jest.spyOn(overlayBackground["authService"], "getAuthStatus").mockResolvedValue(authStatus); + jest.spyOn(overlayBackground as any, "updateOverlayButtonAuthStatus").mockImplementation(); + jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation(); + + await overlayBackground["getAuthStatus"](); + + expect(overlayBackground["authService"].getAuthStatus).toHaveBeenCalled(); + expect(overlayBackground["updateOverlayButtonAuthStatus"]).toHaveBeenCalled(); + expect(overlayBackground["updateOverlayCiphers"]).toHaveBeenCalled(); + expect(overlayBackground["userAuthStatus"]).toBe(authStatus); + }); + }); + + describe("updateOverlayButtonAuthStatus", () => { + it("will send a message to the button port with the user's auth status", () => { + overlayBackground["overlayButtonPort"] = mock(); + jest.spyOn(overlayBackground["overlayButtonPort"], "postMessage"); + + overlayBackground["updateOverlayButtonAuthStatus"](); + + expect(overlayBackground["overlayButtonPort"].postMessage).toHaveBeenCalledWith({ + command: "updateOverlayButtonAuthStatus", + authStatus: overlayBackground["userAuthStatus"], + }); + }); + }); + + describe("getTranslations", () => { + it("will query the overlay page translations if they have not been queried", () => { + overlayBackground["overlayPageTranslations"] = undefined; + jest.spyOn(overlayBackground as any, "getTranslations"); + jest.spyOn(overlayBackground["i18nService"], "translate").mockImplementation((key) => key); + jest.spyOn(BrowserApi, "getUILanguage").mockReturnValue("en"); + + const translations = overlayBackground["getTranslations"](); + + expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); + const translationKeys = [ + "opensInANewWindow", + "bitwardenOverlayButton", + "toggleBitwardenVaultOverlay", + "bitwardenVault", + "unlockYourAccountToViewMatchingLogins", + "unlockAccount", + "fillCredentialsFor", + "partialUsername", + "view", + "noItemsToShow", + "newItem", + "addNewVaultItem", + ]; + translationKeys.forEach((key) => { + expect(overlayBackground["i18nService"].translate).toHaveBeenCalledWith(key); + }); + expect(translations).toStrictEqual({ + locale: "en", + opensInANewWindow: "opensInANewWindow", + buttonPageTitle: "bitwardenOverlayButton", + toggleBitwardenVaultOverlay: "toggleBitwardenVaultOverlay", + listPageTitle: "bitwardenVault", + unlockYourAccount: "unlockYourAccountToViewMatchingLogins", + unlockAccount: "unlockAccount", + fillCredentialsFor: "fillCredentialsFor", + partialUsername: "partialUsername", + view: "view", + noItemsToShow: "noItemsToShow", + newItem: "newItem", + addNewVaultItem: "addNewVaultItem", + }); + }); + }); + + describe("setupExtensionMessageListeners", () => { + it("will set up onMessage and onConnect listeners", () => { + overlayBackground["setupExtensionMessageListeners"](); + + // eslint-disable-next-line + expect(chrome.runtime.onMessage.addListener).toHaveBeenCalled(); + expect(chrome.runtime.onConnect.addListener).toHaveBeenCalled(); + }); + }); + + describe("handleExtensionMessage", () => { + it("will return early if the message command is not present within the extensionMessageHandlers", () => { + const message = { + command: "not-a-command", + }; + const sender = mock({ tab: { id: 1 } }); + const sendResponse = jest.fn(); + + const returnValue = overlayBackground["handleExtensionMessage"]( + message, + sender, + sendResponse, + ); + + expect(returnValue).toBe(null); + expect(sendResponse).not.toHaveBeenCalled(); + }); + + it("will trigger the message handler and return undefined if the message does not have a response", () => { + const message = { + command: "autofillOverlayElementClosed", + }; + const sender = mock({ tab: { id: 1 } }); + const sendResponse = jest.fn(); + jest.spyOn(overlayBackground as any, "overlayElementClosed"); + + const returnValue = overlayBackground["handleExtensionMessage"]( + message, + sender, + sendResponse, + ); + + expect(returnValue).toBe(null); + expect(sendResponse).not.toHaveBeenCalled(); + expect(overlayBackground["overlayElementClosed"]).toHaveBeenCalledWith(message, sender); + }); + + it("will return a response if the message handler returns a response", async () => { + const message = { + command: "openAutofillOverlay", + }; + const sender = mock({ tab: { id: 1 } }); + const sendResponse = jest.fn(); + jest.spyOn(overlayBackground as any, "getTranslations").mockReturnValue("translations"); + + const returnValue = overlayBackground["handleExtensionMessage"]( + message, + sender, + sendResponse, + ); + + expect(returnValue).toBe(true); + }); + + describe("extension message handlers", () => { + beforeEach(() => { + jest + .spyOn(overlayBackground as any, "getAuthStatus") + .mockResolvedValue(AuthenticationStatus.Unlocked); + }); + + describe("openAutofillOverlay message handler", () => { + it("opens the autofill overlay by sending a message to the current tab", async () => { + const sender = mock({ tab: { id: 1 } }); + jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(sender.tab); + jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); + + sendMockExtensionMessage({ command: "openAutofillOverlay" }); + await flushPromises(); + + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + sender.tab, + "openAutofillOverlay", + { + isFocusingFieldElement: false, + isOpeningFullOverlay: false, + authStatus: AuthenticationStatus.Unlocked, + }, + ); + }); + }); + + describe("autofillOverlayElementClosed message handler", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + }); + + it("disconnects any expired ports if the sender is not from the same page as the most recently focused field", () => { + const port1 = mock(); + const port2 = mock(); + overlayBackground["expiredPorts"] = [port1, port2]; + const sender = mock({ tab: { id: 1 } }); + const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage( + { + command: "autofillOverlayElementClosed", + overlayElement: AutofillOverlayElement.Button, + }, + sender, + ); + + expect(port1.disconnect).toHaveBeenCalled(); + expect(port2.disconnect).toHaveBeenCalled(); + }); + + it("disconnects the button element port", () => { + sendMockExtensionMessage({ + command: "autofillOverlayElementClosed", + overlayElement: AutofillOverlayElement.Button, + }); + + expect(buttonPortSpy.disconnect).toHaveBeenCalled(); + expect(overlayBackground["overlayButtonPort"]).toBeNull(); + }); + + it("disconnects the list element port", () => { + sendMockExtensionMessage({ + command: "autofillOverlayElementClosed", + overlayElement: AutofillOverlayElement.List, + }); + + expect(listPortSpy.disconnect).toHaveBeenCalled(); + expect(overlayBackground["overlayListPort"]).toBeNull(); + }); + }); + + describe("autofillOverlayAddNewVaultItem message handler", () => { + let sender: chrome.runtime.MessageSender; + beforeEach(() => { + sender = mock({ tab: { id: 1 } }); + jest + .spyOn(overlayBackground["cipherService"], "setAddEditCipherInfo") + .mockImplementation(); + jest.spyOn(overlayBackground as any, "openAddEditVaultItemPopout").mockImplementation(); + }); + + it("will not open the add edit popout window if the message does not have a login cipher provided", () => { + sendMockExtensionMessage({ command: "autofillOverlayAddNewVaultItem" }, sender); + + expect(overlayBackground["cipherService"].setAddEditCipherInfo).not.toHaveBeenCalled(); + expect(overlayBackground["openAddEditVaultItemPopout"]).not.toHaveBeenCalled(); + }); + + it("will open the add edit popout window after creating a new cipher", async () => { + jest.spyOn(BrowserApi, "sendMessage"); + + sendMockExtensionMessage( + { + command: "autofillOverlayAddNewVaultItem", + login: { + uri: "https://tacos.com", + hostname: "", + username: "username", + password: "password", + }, + }, + sender, + ); + await flushPromises(); + + expect(overlayBackground["cipherService"].setAddEditCipherInfo).toHaveBeenCalled(); + expect(BrowserApi.sendMessage).toHaveBeenCalledWith( + "inlineAutofillMenuRefreshAddEditCipher", + ); + expect(overlayBackground["openAddEditVaultItemPopout"]).toHaveBeenCalled(); + }); + }); + + describe("getAutofillOverlayVisibility message handler", () => { + beforeEach(() => { + jest + .spyOn(overlayBackground as any, "getOverlayVisibility") + .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); + }); + + it("will set the overlayVisibility property", async () => { + sendMockExtensionMessage({ command: "getAutofillOverlayVisibility" }); + await flushPromises(); + + expect(await overlayBackground["getOverlayVisibility"]()).toBe( + AutofillOverlayVisibility.OnFieldFocus, + ); + }); + + it("returns the overlayVisibility property", async () => { + const sendMessageSpy = jest.fn(); + + sendMockExtensionMessage( + { command: "getAutofillOverlayVisibility" }, + undefined, + sendMessageSpy, + ); + await flushPromises(); + + expect(sendMessageSpy).toHaveBeenCalledWith(AutofillOverlayVisibility.OnFieldFocus); + }); + }); + + describe("checkAutofillOverlayFocused message handler", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + }); + + it("will check if the overlay list is focused if the list port is open", () => { + sendMockExtensionMessage({ command: "checkAutofillOverlayFocused" }); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillOverlayListFocused", + }); + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillOverlayButtonFocused", + }); + }); + + it("will check if the overlay button is focused if the list port is not open", () => { + overlayBackground["overlayListPort"] = undefined; + + sendMockExtensionMessage({ command: "checkAutofillOverlayFocused" }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillOverlayButtonFocused", + }); + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillOverlayListFocused", + }); + }); + }); + + describe("focusAutofillOverlayList message handler", () => { + it("will send a `focusOverlayList` message to the overlay list port", async () => { + await initOverlayElementPorts({ initList: true, initButton: false }); + + sendMockExtensionMessage({ command: "focusAutofillOverlayList" }); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "focusOverlayList" }); + }); + }); + + describe("updateAutofillOverlayPosition message handler", () => { + beforeEach(async () => { + await overlayBackground["handlePortOnConnect"]( + createPortSpyMock(AutofillOverlayPort.List), + ); + listPortSpy = overlayBackground["overlayListPort"]; + + await overlayBackground["handlePortOnConnect"]( + createPortSpyMock(AutofillOverlayPort.Button), + ); + buttonPortSpy = overlayBackground["overlayButtonPort"]; + }); + + it("ignores updating the position if the overlay element type is not provided", () => { + sendMockExtensionMessage({ command: "updateAutofillOverlayPosition" }); + + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + }); + + it("skips updating the position if the most recently focused field is different than the message sender", () => { + const sender = mock({ tab: { id: 1 } }); + const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ command: "updateAutofillOverlayPosition" }, sender); + + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + }); + + it("updates the overlay button's position", () => { + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillOverlayPosition", + overlayElement: AutofillOverlayElement.Button, + }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: { height: "2px", left: "4px", top: "2px", width: "2px" }, + }); + }); + + it("modifies the overlay button's height for medium sized input elements", () => { + const focusedFieldData = createFocusedFieldDataMock({ + focusedFieldRects: { top: 1, left: 2, height: 35, width: 4 }, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillOverlayPosition", + overlayElement: AutofillOverlayElement.Button, + }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: { height: "20px", left: "-22px", top: "8px", width: "20px" }, + }); + }); + + it("modifies the overlay button's height for large sized input elements", () => { + const focusedFieldData = createFocusedFieldDataMock({ + focusedFieldRects: { top: 1, left: 2, height: 50, width: 4 }, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillOverlayPosition", + overlayElement: AutofillOverlayElement.Button, + }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: { height: "27px", left: "-32px", top: "13px", width: "27px" }, + }); + }); + + it("takes into account the right padding of the focused field in positioning the button if the right padding of the field is larger than the left padding", () => { + const focusedFieldData = createFocusedFieldDataMock({ + focusedFieldStyles: { paddingRight: "20px", paddingLeft: "6px" }, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillOverlayPosition", + overlayElement: AutofillOverlayElement.Button, + }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: { height: "2px", left: "-18px", top: "2px", width: "2px" }, + }); + }); + + it("will post a message to the overlay list facilitating an update of the list's position", () => { + const sender = mock({ tab: { id: 1 } }); + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + overlayBackground["updateOverlayPosition"]( + { overlayElement: AutofillOverlayElement.List }, + sender, + ); + sendMockExtensionMessage({ + command: "updateAutofillOverlayPosition", + overlayElement: AutofillOverlayElement.List, + }); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: { left: "2px", top: "4px", width: "4px" }, + }); + }); + }); + + describe("updateOverlayHidden", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + }); + + it("returns early if the display value is not provided", () => { + const message = { + command: "updateAutofillOverlayHidden", + }; + + sendMockExtensionMessage(message); + + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith(message); + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith(message); + }); + + it("posts a message to the overlay button and list with the display value", () => { + const message = { command: "updateAutofillOverlayHidden", display: "none" }; + + sendMockExtensionMessage(message); + + expect(overlayBackground["overlayButtonPort"].postMessage).toHaveBeenCalledWith({ + command: "updateOverlayHidden", + styles: { + display: message.display, + }, + }); + expect(overlayBackground["overlayListPort"].postMessage).toHaveBeenCalledWith({ + command: "updateOverlayHidden", + styles: { + display: message.display, + }, + }); + }); + }); + + describe("collectPageDetailsResponse message handler", () => { + let sender: chrome.runtime.MessageSender; + const pageDetails1 = createAutofillPageDetailsMock({ + login: { username: "username1", password: "password1" }, + }); + const pageDetails2 = createAutofillPageDetailsMock({ + login: { username: "username2", password: "password2" }, + }); + + beforeEach(() => { + sender = mock({ tab: { id: 1 } }); + }); + + it("stores the page details provided by the message by the tab id of the sender", () => { + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: pageDetails1 }, + sender, + ); + + expect(overlayBackground["pageDetailsForTab"][sender.tab.id]).toStrictEqual( + new Map([ + [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], + ]), + ); + }); + + it("updates the page details for a tab that already has a set of page details stored ", () => { + const secondFrameSender = mock({ + tab: { id: 1 }, + frameId: 3, + }); + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], + ]); + + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: pageDetails2 }, + secondFrameSender, + ); + + expect(overlayBackground["pageDetailsForTab"][sender.tab.id]).toStrictEqual( + new Map([ + [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], + [ + secondFrameSender.frameId, + { + frameId: secondFrameSender.frameId, + tab: secondFrameSender.tab, + details: pageDetails2, + }, + ], + ]), + ); + }); + }); + + describe("unlockCompleted message handler", () => { + let getAuthStatusSpy: jest.SpyInstance; + + beforeEach(() => { + overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; + jest.spyOn(BrowserApi, "tabSendMessageData"); + getAuthStatusSpy = jest + .spyOn(overlayBackground as any, "getAuthStatus") + .mockImplementation(() => { + overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; + return Promise.resolve(AuthenticationStatus.Unlocked); + }); + }); + + it("updates the user's auth status but does not open the overlay", async () => { + const message = { + command: "unlockCompleted", + data: { + commandToRetry: { message: { command: "" } }, + }, + }; + + sendMockExtensionMessage(message); + await flushPromises(); + + expect(getAuthStatusSpy).toHaveBeenCalled(); + expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled(); + }); + + it("updates user's auth status and opens the overlay if a follow up command is provided", async () => { + const sender = mock({ tab: { id: 1 } }); + const message = { + command: "unlockCompleted", + data: { + commandToRetry: { message: { command: "openAutofillOverlay" } }, + }, + }; + jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(sender.tab); + + sendMockExtensionMessage(message); + await flushPromises(); + + expect(getAuthStatusSpy).toHaveBeenCalled(); + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + sender.tab, + "openAutofillOverlay", + { + isFocusingFieldElement: true, + isOpeningFullOverlay: false, + authStatus: AuthenticationStatus.Unlocked, + }, + ); + }); + }); + + describe("extension messages that trigger an update of the inline menu ciphers", () => { + const extensionMessages = [ + "addedCipher", + "addEditCipherSubmitted", + "editedCipher", + "deletedCipher", + ]; + + beforeEach(() => { + jest.spyOn(overlayBackground, "updateOverlayCiphers").mockImplementation(); + }); + + extensionMessages.forEach((message) => { + it(`triggers an update of the overlay ciphers when the ${message} message is received`, () => { + sendMockExtensionMessage({ command: message }); + expect(overlayBackground.updateOverlayCiphers).toHaveBeenCalled(); + }); + }); + }); + }); + }); + + describe("handlePortOnConnect", () => { + beforeEach(() => { + jest.spyOn(overlayBackground as any, "updateOverlayPosition").mockImplementation(); + jest.spyOn(overlayBackground as any, "getAuthStatus").mockImplementation(); + jest.spyOn(overlayBackground as any, "getTranslations").mockImplementation(); + jest.spyOn(overlayBackground as any, "getOverlayCipherData").mockImplementation(); + }); + + it("skips setting up the overlay port if the port connection is not for an overlay element", async () => { + const port = createPortSpyMock("not-an-overlay-element"); + + await overlayBackground["handlePortOnConnect"](port); + + expect(port.onMessage.addListener).not.toHaveBeenCalled(); + expect(port.postMessage).not.toHaveBeenCalled(); + }); + + it("sets up the overlay list port if the port connection is for the overlay list", async () => { + await initOverlayElementPorts({ initList: true, initButton: false }); + await flushPromises(); + + expect(overlayBackground["overlayButtonPort"]).toBeUndefined(); + expect(listPortSpy.onMessage.addListener).toHaveBeenCalled(); + expect(listPortSpy.postMessage).toHaveBeenCalled(); + expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); + expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/list.css"); + expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); + expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled(); + expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith( + { overlayElement: AutofillOverlayElement.List }, + listPortSpy.sender, + ); + }); + + it("sets up the overlay button port if the port connection is for the overlay button", async () => { + await initOverlayElementPorts({ initList: false, initButton: true }); + await flushPromises(); + + expect(overlayBackground["overlayListPort"]).toBeUndefined(); + expect(buttonPortSpy.onMessage.addListener).toHaveBeenCalled(); + expect(buttonPortSpy.postMessage).toHaveBeenCalled(); + expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); + expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/button.css"); + expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); + expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith( + { overlayElement: AutofillOverlayElement.Button }, + buttonPortSpy.sender, + ); + }); + + it("stores an existing overlay port so that it can be disconnected at a later time", async () => { + overlayBackground["overlayButtonPort"] = mock(); + + await initOverlayElementPorts({ initList: false, initButton: true }); + await flushPromises(); + + expect(overlayBackground["expiredPorts"].length).toBe(1); + }); + + it("gets the system theme", async () => { + themeStateService.selectedTheme$ = of(ThemeType.System); + + await initOverlayElementPorts({ initList: true, initButton: false }); + await flushPromises(); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ theme: ThemeType.System }), + ); + }); + }); + + describe("handleOverlayElementPortMessage", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; + }); + + it("ignores port messages that do not contain a handler", () => { + jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); + + sendPortMessage(buttonPortSpy, { command: "checkAutofillOverlayButtonFocused" }); + + expect(overlayBackground["checkOverlayButtonFocused"]).not.toHaveBeenCalled(); + }); + + describe("overlay button message handlers", () => { + it("unlocks the vault if the user auth status is not unlocked", () => { + overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; + jest.spyOn(overlayBackground as any, "unlockVault").mockImplementation(); + + sendPortMessage(buttonPortSpy, { command: "overlayButtonClicked" }); + + expect(overlayBackground["unlockVault"]).toHaveBeenCalled(); + }); + + it("opens the autofill overlay if the auth status is unlocked", () => { + jest.spyOn(overlayBackground as any, "openOverlay").mockImplementation(); + + sendPortMessage(buttonPortSpy, { command: "overlayButtonClicked" }); + + expect(overlayBackground["openOverlay"]).toHaveBeenCalled(); + }); + + describe("closeAutofillOverlay", () => { + it("sends a `closeOverlay` message to the sender tab", () => { + jest.spyOn(BrowserApi, "tabSendMessageData"); + + sendPortMessage(buttonPortSpy, { command: "closeAutofillOverlay" }); + + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + buttonPortSpy.sender.tab, + "closeAutofillOverlay", + { forceCloseOverlay: false }, + ); + }); + }); + + describe("forceCloseAutofillOverlay", () => { + it("sends a `closeOverlay` message to the sender tab with a `forceCloseOverlay` flag of `true` set", () => { + jest.spyOn(BrowserApi, "tabSendMessageData"); + + sendPortMessage(buttonPortSpy, { command: "forceCloseAutofillOverlay" }); + + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + buttonPortSpy.sender.tab, + "closeAutofillOverlay", + { forceCloseOverlay: true }, + ); + }); + }); + + describe("overlayPageBlurred", () => { + it("checks if the overlay list is focused", () => { + jest.spyOn(overlayBackground as any, "checkOverlayListFocused"); + + sendPortMessage(buttonPortSpy, { command: "overlayPageBlurred" }); + + expect(overlayBackground["checkOverlayListFocused"]).toHaveBeenCalled(); + }); + }); + + describe("redirectOverlayFocusOut", () => { + beforeEach(() => { + jest.spyOn(BrowserApi, "tabSendMessageData"); + }); + + it("ignores the redirect message if the direction is not provided", () => { + sendPortMessage(buttonPortSpy, { command: "redirectOverlayFocusOut" }); + + expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled(); + }); + + it("sends the redirect message if the direction is provided", () => { + sendPortMessage(buttonPortSpy, { + command: "redirectOverlayFocusOut", + direction: RedirectFocusDirection.Next, + }); + + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + buttonPortSpy.sender.tab, + "redirectOverlayFocusOut", + { direction: RedirectFocusDirection.Next }, + ); + }); + }); + }); + + describe("overlay list message handlers", () => { + describe("checkAutofillOverlayButtonFocused", () => { + it("checks on the focus state of the overlay button", () => { + jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); + + sendPortMessage(listPortSpy, { command: "checkAutofillOverlayButtonFocused" }); + + expect(overlayBackground["checkOverlayButtonFocused"]).toHaveBeenCalled(); + }); + }); + + describe("forceCloseAutofillOverlay", () => { + it("sends a `closeOverlay` message to the sender tab with a `forceCloseOverlay` flag of `true` set", () => { + jest.spyOn(BrowserApi, "tabSendMessageData"); + + sendPortMessage(listPortSpy, { command: "forceCloseAutofillOverlay" }); + + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + listPortSpy.sender.tab, + "closeAutofillOverlay", + { forceCloseOverlay: true }, + ); + }); + }); + + describe("overlayPageBlurred", () => { + it("checks on the focus state of the overlay button", () => { + jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); + + sendPortMessage(listPortSpy, { command: "overlayPageBlurred" }); + + expect(overlayBackground["checkOverlayButtonFocused"]).toHaveBeenCalled(); + }); + }); + + describe("unlockVault", () => { + it("closes the autofill overlay and opens the unlock popout", async () => { + jest.spyOn(overlayBackground as any, "closeOverlay").mockImplementation(); + jest.spyOn(overlayBackground as any, "openUnlockPopout").mockImplementation(); + jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); + + sendPortMessage(listPortSpy, { command: "unlockVault" }); + await flushPromises(); + + expect(overlayBackground["closeOverlay"]).toHaveBeenCalledWith(listPortSpy); + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + listPortSpy.sender.tab, + "addToLockedVaultPendingNotifications", + { + commandToRetry: { + message: { command: "openAutofillOverlay" }, + sender: listPortSpy.sender, + }, + target: "overlay.background", + }, + ); + expect(overlayBackground["openUnlockPopout"]).toHaveBeenCalledWith( + listPortSpy.sender.tab, + true, + ); + }); + }); + + describe("fillSelectedListItem", () => { + let getLoginCiphersSpy: jest.SpyInstance; + let isPasswordRepromptRequiredSpy: jest.SpyInstance; + let doAutoFillSpy: jest.SpyInstance; + let sender: chrome.runtime.MessageSender; + const pageDetails = createAutofillPageDetailsMock({ + login: { username: "username1", password: "password1" }, + }); + + beforeEach(() => { + getLoginCiphersSpy = jest.spyOn(overlayBackground["overlayLoginCiphers"], "get"); + isPasswordRepromptRequiredSpy = jest.spyOn( + overlayBackground["autofillService"], + "isPasswordRepromptRequired", + ); + doAutoFillSpy = jest.spyOn(overlayBackground["autofillService"], "doAutoFill"); + sender = mock({ tab: { id: 1 } }); + }); + + it("ignores the fill request if the overlay cipher id is not provided", async () => { + sendPortMessage(listPortSpy, { command: "fillSelectedListItem" }); + await flushPromises(); + + expect(getLoginCiphersSpy).not.toHaveBeenCalled(); + expect(isPasswordRepromptRequiredSpy).not.toHaveBeenCalled(); + expect(doAutoFillSpy).not.toHaveBeenCalled(); + }); + + it("ignores the fill request if the tab does not contain any identified page details", async () => { + sendPortMessage(listPortSpy, { + command: "fillSelectedListItem", + overlayCipherId: "overlay-cipher-1", + }); + await flushPromises(); + + expect(getLoginCiphersSpy).not.toHaveBeenCalled(); + expect(isPasswordRepromptRequiredSpy).not.toHaveBeenCalled(); + expect(doAutoFillSpy).not.toHaveBeenCalled(); + }); + + it("ignores the fill request if a master password reprompt is required", async () => { + const cipher = mock({ + reprompt: CipherRepromptType.Password, + type: CipherType.Login, + }); + overlayBackground["overlayLoginCiphers"] = new Map([["overlay-cipher-1", cipher]]); + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], + ]); + getLoginCiphersSpy = jest.spyOn(overlayBackground["overlayLoginCiphers"], "get"); + isPasswordRepromptRequiredSpy.mockResolvedValue(true); + + sendPortMessage(listPortSpy, { + command: "fillSelectedListItem", + overlayCipherId: "overlay-cipher-1", + }); + await flushPromises(); + + expect(getLoginCiphersSpy).toHaveBeenCalled(); + expect(isPasswordRepromptRequiredSpy).toHaveBeenCalledWith( + cipher, + listPortSpy.sender.tab, + ); + expect(doAutoFillSpy).not.toHaveBeenCalled(); + }); + + it("auto-fills the selected cipher and move it to the top of the front of the ciphers map", async () => { + const cipher1 = mock({ id: "overlay-cipher-1" }); + const cipher2 = mock({ id: "overlay-cipher-2" }); + const cipher3 = mock({ id: "overlay-cipher-3" }); + overlayBackground["overlayLoginCiphers"] = new Map([ + ["overlay-cipher-1", cipher1], + ["overlay-cipher-2", cipher2], + ["overlay-cipher-3", cipher3], + ]); + const pageDetailsForTab = { + frameId: sender.frameId, + tab: sender.tab, + details: pageDetails, + }; + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, pageDetailsForTab], + ]); + isPasswordRepromptRequiredSpy.mockResolvedValue(false); + + sendPortMessage(listPortSpy, { + command: "fillSelectedListItem", + overlayCipherId: "overlay-cipher-2", + }); + await flushPromises(); + + expect(isPasswordRepromptRequiredSpy).toHaveBeenCalledWith( + cipher2, + listPortSpy.sender.tab, + ); + expect(doAutoFillSpy).toHaveBeenCalledWith({ + tab: listPortSpy.sender.tab, + cipher: cipher2, + pageDetails: [pageDetailsForTab], + fillNewPassword: true, + allowTotpAutofill: true, + }); + expect(overlayBackground["overlayLoginCiphers"].entries()).toStrictEqual( + new Map([ + ["overlay-cipher-2", cipher2], + ["overlay-cipher-1", cipher1], + ["overlay-cipher-3", cipher3], + ]).entries(), + ); + }); + + it("copies the cipher's totp code to the clipboard after filling", async () => { + const cipher1 = mock({ id: "overlay-cipher-1" }); + overlayBackground["overlayLoginCiphers"] = new Map([["overlay-cipher-1", cipher1]]); + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], + ]); + isPasswordRepromptRequiredSpy.mockResolvedValue(false); + const copyToClipboardSpy = jest + .spyOn(overlayBackground["platformUtilsService"], "copyToClipboard") + .mockImplementation(); + doAutoFillSpy.mockReturnValueOnce("totp-code"); + + sendPortMessage(listPortSpy, { + command: "fillSelectedListItem", + overlayCipherId: "overlay-cipher-2", + }); + await flushPromises(); + + expect(copyToClipboardSpy).toHaveBeenCalledWith("totp-code"); + }); + }); + + describe("getNewVaultItemDetails", () => { + it("will send an addNewVaultItemFromOverlay message", async () => { + jest.spyOn(BrowserApi, "tabSendMessage"); + + sendPortMessage(listPortSpy, { command: "addNewVaultItem" }); + await flushPromises(); + + expect(BrowserApi.tabSendMessage).toHaveBeenCalledWith(listPortSpy.sender.tab, { + command: "addNewVaultItemFromOverlay", + }); + }); + }); + + describe("viewSelectedCipher", () => { + let openViewVaultItemPopoutSpy: jest.SpyInstance; + + beforeEach(() => { + openViewVaultItemPopoutSpy = jest + .spyOn(overlayBackground as any, "openViewVaultItemPopout") + .mockImplementation(); + }); + + it("returns early if the passed cipher ID does not match one of the overlay login ciphers", async () => { + overlayBackground["overlayLoginCiphers"] = new Map([ + ["overlay-cipher-0", mock({ id: "overlay-cipher-0" })], + ]); + + sendPortMessage(listPortSpy, { + command: "viewSelectedCipher", + overlayCipherId: "overlay-cipher-1", + }); + await flushPromises(); + + expect(openViewVaultItemPopoutSpy).not.toHaveBeenCalled(); + }); + + it("will open the view vault item popout with the selected cipher", async () => { + const cipher = mock({ id: "overlay-cipher-1" }); + overlayBackground["overlayLoginCiphers"] = new Map([ + ["overlay-cipher-0", mock({ id: "overlay-cipher-0" })], + ["overlay-cipher-1", cipher], + ]); + + sendPortMessage(listPortSpy, { + command: "viewSelectedCipher", + overlayCipherId: "overlay-cipher-1", + }); + await flushPromises(); + + expect(overlayBackground["openViewVaultItemPopout"]).toHaveBeenCalledWith( + listPortSpy.sender.tab, + { + cipherId: cipher.id, + action: SHOW_AUTOFILL_BUTTON, + }, + ); + }); + }); + + describe("redirectOverlayFocusOut", () => { + it("redirects focus out of the overlay list", async () => { + const message = { + command: "redirectOverlayFocusOut", + direction: RedirectFocusDirection.Next, + }; + const redirectOverlayFocusOutSpy = jest.spyOn( + overlayBackground as any, + "redirectOverlayFocusOut", + ); + + sendPortMessage(listPortSpy, message); + await flushPromises(); + + expect(redirectOverlayFocusOutSpy).toHaveBeenCalledWith(message, listPortSpy); + }); + }); + }); + }); +}); diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts new file mode 100644 index 00000000000..1a5d49e9e1f --- /dev/null +++ b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts @@ -0,0 +1,798 @@ +import { firstValueFrom } from "rxjs"; + +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { SHOW_AUTOFILL_BUTTON } from "@bitwarden/common/autofill/constants"; +import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; +import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; + +import { openUnlockPopout } from "../../../auth/popup/utils/auth-popout-window"; +import { BrowserApi } from "../../../platform/browser/browser-api"; +import { + openViewVaultItemPopout, + openAddEditVaultItemPopout, +} from "../../../vault/popup/utils/vault-popout-window"; +import { LockedVaultPendingNotificationsData } from "../../background/abstractions/notification.background"; +import { OverlayBackground as OverlayBackgroundInterface } from "../../background/abstractions/overlay.background"; +import { AutofillOverlayElement, AutofillOverlayPort } from "../../enums/autofill-overlay.enum"; +import { AutofillService, PageDetail } from "../../services/abstractions/autofill.service"; + +import { + FocusedFieldData, + OverlayBackgroundExtensionMessageHandlers, + OverlayButtonPortMessageHandlers, + OverlayCipherData, + OverlayListPortMessageHandlers, + OverlayBackgroundExtensionMessage, + OverlayAddNewItemMessage, + OverlayPortMessage, + WebsiteIconData, +} from "./abstractions/overlay.background.deprecated"; + +class LegacyOverlayBackground implements OverlayBackgroundInterface { + private readonly openUnlockPopout = openUnlockPopout; + private readonly openViewVaultItemPopout = openViewVaultItemPopout; + private readonly openAddEditVaultItemPopout = openAddEditVaultItemPopout; + private overlayLoginCiphers: Map = new Map(); + private pageDetailsForTab: Record< + chrome.runtime.MessageSender["tab"]["id"], + Map + > = {}; + private userAuthStatus: AuthenticationStatus = AuthenticationStatus.LoggedOut; + private overlayButtonPort: chrome.runtime.Port; + private overlayListPort: chrome.runtime.Port; + private expiredPorts: chrome.runtime.Port[] = []; + private focusedFieldData: FocusedFieldData; + private overlayPageTranslations: Record; + private iconsServerUrl: string; + private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = { + openAutofillOverlay: () => this.openOverlay(false), + autofillOverlayElementClosed: ({ message, sender }) => + this.overlayElementClosed(message, sender), + autofillOverlayAddNewVaultItem: ({ message, sender }) => this.addNewVaultItem(message, sender), + getAutofillOverlayVisibility: () => this.getOverlayVisibility(), + checkAutofillOverlayFocused: () => this.checkOverlayFocused(), + focusAutofillOverlayList: () => this.focusOverlayList(), + updateAutofillOverlayPosition: ({ message, sender }) => + this.updateOverlayPosition(message, sender), + updateAutofillOverlayHidden: ({ message }) => this.updateOverlayHidden(message), + updateFocusedFieldData: ({ message, sender }) => this.setFocusedFieldData(message, sender), + collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender), + unlockCompleted: ({ message }) => this.unlockCompleted(message), + addedCipher: () => this.updateOverlayCiphers(), + addEditCipherSubmitted: () => this.updateOverlayCiphers(), + editedCipher: () => this.updateOverlayCiphers(), + deletedCipher: () => this.updateOverlayCiphers(), + }; + private readonly overlayButtonPortMessageHandlers: OverlayButtonPortMessageHandlers = { + overlayButtonClicked: ({ port }) => this.handleOverlayButtonClicked(port), + closeAutofillOverlay: ({ port }) => this.closeOverlay(port), + forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true), + overlayPageBlurred: () => this.checkOverlayListFocused(), + redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port), + }; + private readonly overlayListPortMessageHandlers: OverlayListPortMessageHandlers = { + checkAutofillOverlayButtonFocused: () => this.checkOverlayButtonFocused(), + forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true), + overlayPageBlurred: () => this.checkOverlayButtonFocused(), + unlockVault: ({ port }) => this.unlockVault(port), + fillSelectedListItem: ({ message, port }) => this.fillSelectedOverlayListItem(message, port), + addNewVaultItem: ({ port }) => this.getNewVaultItemDetails(port), + viewSelectedCipher: ({ message, port }) => this.viewSelectedCipher(message, port), + redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port), + }; + + constructor( + private cipherService: CipherService, + private autofillService: AutofillService, + private authService: AuthService, + private environmentService: EnvironmentService, + private domainSettingsService: DomainSettingsService, + private autofillSettingsService: AutofillSettingsServiceAbstraction, + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + private themeStateService: ThemeStateService, + ) {} + + /** + * Removes cached page details for a tab + * based on the passed tabId. + * + * @param tabId - Used to reference the page details of a specific tab + */ + removePageDetails(tabId: number) { + if (!this.pageDetailsForTab[tabId]) { + return; + } + + this.pageDetailsForTab[tabId].clear(); + delete this.pageDetailsForTab[tabId]; + } + + /** + * Sets up the extension message listeners and gets the settings for the + * overlay's visibility and the user's authentication status. + */ + async init() { + this.setupExtensionMessageListeners(); + const env = await firstValueFrom(this.environmentService.environment$); + this.iconsServerUrl = env.getIconsUrl(); + await this.getOverlayVisibility(); + await this.getAuthStatus(); + } + + /** + * Updates the overlay list's ciphers and sends the updated list to the overlay list iframe. + * Queries all ciphers for the given url, and sorts them by last used. Will not update the + * list of ciphers if the extension is not unlocked. + */ + async updateOverlayCiphers() { + const authStatus = await firstValueFrom(this.authService.activeAccountStatus$); + if (authStatus !== AuthenticationStatus.Unlocked) { + return; + } + + const currentTab = await BrowserApi.getTabFromCurrentWindowId(); + if (!currentTab?.url) { + return; + } + + this.overlayLoginCiphers = new Map(); + const ciphersViews = (await this.cipherService.getAllDecryptedForUrl(currentTab.url)).sort( + (a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b), + ); + for (let cipherIndex = 0; cipherIndex < ciphersViews.length; cipherIndex++) { + this.overlayLoginCiphers.set(`overlay-cipher-${cipherIndex}`, ciphersViews[cipherIndex]); + } + + const ciphers = await this.getOverlayCipherData(); + this.overlayListPort?.postMessage({ command: "updateOverlayListCiphers", ciphers }); + await BrowserApi.tabSendMessageData(currentTab, "updateIsOverlayCiphersPopulated", { + isOverlayCiphersPopulated: Boolean(ciphers.length), + }); + } + + /** + * Strips out unnecessary data from the ciphers and returns an array of + * objects that contain the cipher data needed for the overlay list. + */ + private async getOverlayCipherData(): Promise { + const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$); + const overlayCiphersArray = Array.from(this.overlayLoginCiphers); + const overlayCipherData: OverlayCipherData[] = []; + let loginCipherIcon: WebsiteIconData; + + for (let cipherIndex = 0; cipherIndex < overlayCiphersArray.length; cipherIndex++) { + const [overlayCipherId, cipher] = overlayCiphersArray[cipherIndex]; + if (!loginCipherIcon && cipher.type === CipherType.Login) { + loginCipherIcon = buildCipherIcon(this.iconsServerUrl, cipher, showFavicons); + } + + overlayCipherData.push({ + id: overlayCipherId, + name: cipher.name, + type: cipher.type, + reprompt: cipher.reprompt, + favorite: cipher.favorite, + icon: + cipher.type === CipherType.Login + ? loginCipherIcon + : buildCipherIcon(this.iconsServerUrl, cipher, showFavicons), + login: cipher.type === CipherType.Login ? { username: cipher.login.username } : null, + card: cipher.type === CipherType.Card ? cipher.card.subTitle : null, + }); + } + + return overlayCipherData; + } + + /** + * Handles aggregation of page details for a tab. Stores the page details + * in association with the tabId of the tab that sent the message. + * + * @param message - Message received from the `collectPageDetailsResponse` command + * @param sender - The sender of the message + */ + private storePageDetails( + message: OverlayBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + const pageDetails = { + frameId: sender.frameId, + tab: sender.tab, + details: message.details, + }; + + const pageDetailsMap = this.pageDetailsForTab[sender.tab.id]; + if (!pageDetailsMap) { + this.pageDetailsForTab[sender.tab.id] = new Map([[sender.frameId, pageDetails]]); + return; + } + + pageDetailsMap.set(sender.frameId, pageDetails); + } + + /** + * Triggers autofill for the selected cipher in the overlay list. Also places + * the selected cipher at the top of the list of ciphers. + * + * @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID. + * @param sender - The sender of the port message + */ + private async fillSelectedOverlayListItem( + { overlayCipherId }: OverlayPortMessage, + { sender }: chrome.runtime.Port, + ) { + const pageDetails = this.pageDetailsForTab[sender.tab.id]; + if (!overlayCipherId || !pageDetails?.size) { + return; + } + + const cipher = this.overlayLoginCiphers.get(overlayCipherId); + + if (await this.autofillService.isPasswordRepromptRequired(cipher, sender.tab)) { + return; + } + const totpCode = await this.autofillService.doAutoFill({ + tab: sender.tab, + cipher: cipher, + pageDetails: Array.from(pageDetails.values()), + fillNewPassword: true, + allowTotpAutofill: true, + }); + + if (totpCode) { + this.platformUtilsService.copyToClipboard(totpCode); + } + + this.overlayLoginCiphers = new Map([[overlayCipherId, cipher], ...this.overlayLoginCiphers]); + } + + /** + * Checks if the overlay is focused. Will check the overlay list + * if it is open, otherwise it will check the overlay button. + */ + private checkOverlayFocused() { + if (this.overlayListPort) { + this.checkOverlayListFocused(); + + return; + } + + this.checkOverlayButtonFocused(); + } + + /** + * Posts a message to the overlay button iframe to check if it is focused. + */ + private checkOverlayButtonFocused() { + this.overlayButtonPort?.postMessage({ command: "checkAutofillOverlayButtonFocused" }); + } + + /** + * Posts a message to the overlay list iframe to check if it is focused. + */ + private checkOverlayListFocused() { + this.overlayListPort?.postMessage({ command: "checkAutofillOverlayListFocused" }); + } + + /** + * Sends a message to the sender tab to close the autofill overlay. + * + * @param sender - The sender of the port message + * @param forceCloseOverlay - Identifies whether the overlay should be force closed + */ + private closeOverlay({ sender }: chrome.runtime.Port, forceCloseOverlay = false) { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + BrowserApi.tabSendMessageData(sender.tab, "closeAutofillOverlay", { forceCloseOverlay }); + } + + /** + * Handles cleanup when an overlay element is closed. Disconnects + * the list and button ports and sets them to null. + * + * @param overlayElement - The overlay element that was closed, either the list or button + * @param sender - The sender of the port message + */ + private overlayElementClosed( + { overlayElement }: OverlayBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + if (sender.tab.id !== this.focusedFieldData?.tabId) { + this.expiredPorts.forEach((port) => port.disconnect()); + this.expiredPorts = []; + return; + } + + if (overlayElement === AutofillOverlayElement.Button) { + this.overlayButtonPort?.disconnect(); + this.overlayButtonPort = null; + + return; + } + + this.overlayListPort?.disconnect(); + this.overlayListPort = null; + } + + /** + * Updates the position of either the overlay list or button. The position + * is based on the focused field's position and dimensions. + * + * @param overlayElement - The overlay element to update, either the list or button + * @param sender - The sender of the port message + */ + private updateOverlayPosition( + { overlayElement }: { overlayElement?: string }, + sender: chrome.runtime.MessageSender, + ) { + if (!overlayElement || sender.tab.id !== this.focusedFieldData?.tabId) { + return; + } + + if (overlayElement === AutofillOverlayElement.Button) { + this.overlayButtonPort?.postMessage({ + command: "updateIframePosition", + styles: this.getOverlayButtonPosition(), + }); + + return; + } + + this.overlayListPort?.postMessage({ + command: "updateIframePosition", + styles: this.getOverlayListPosition(), + }); + } + + /** + * Gets the position of the focused field and calculates the position + * of the overlay button based on the focused field's position and dimensions. + */ + private getOverlayButtonPosition() { + if (!this.focusedFieldData) { + return; + } + + const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; + const { paddingRight, paddingLeft } = this.focusedFieldData.focusedFieldStyles; + let elementOffset = height * 0.37; + if (height >= 35) { + elementOffset = height >= 50 ? height * 0.47 : height * 0.42; + } + + const elementHeight = height - elementOffset; + const elementTopPosition = top + elementOffset / 2; + let elementLeftPosition = left + width - height + elementOffset / 2; + + const fieldPaddingRight = parseInt(paddingRight, 10); + const fieldPaddingLeft = parseInt(paddingLeft, 10); + if (fieldPaddingRight > fieldPaddingLeft) { + elementLeftPosition = left + width - height - (fieldPaddingRight - elementOffset + 2); + } + + return { + top: `${Math.round(elementTopPosition)}px`, + left: `${Math.round(elementLeftPosition)}px`, + height: `${Math.round(elementHeight)}px`, + width: `${Math.round(elementHeight)}px`, + }; + } + + /** + * Gets the position of the focused field and calculates the position + * of the overlay list based on the focused field's position and dimensions. + */ + private getOverlayListPosition() { + if (!this.focusedFieldData) { + return; + } + + const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; + return { + width: `${Math.round(width)}px`, + top: `${Math.round(top + height)}px`, + left: `${Math.round(left)}px`, + }; + } + + /** + * Sets the focused field data to the data passed in the extension message. + * + * @param focusedFieldData - Contains the rects and styles of the focused field. + * @param sender - The sender of the extension message + */ + private setFocusedFieldData( + { focusedFieldData }: OverlayBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id }; + } + + /** + * Updates the overlay's visibility based on the display property passed in the extension message. + * + * @param display - The display property of the overlay, either "block" or "none" + */ + private updateOverlayHidden({ display }: OverlayBackgroundExtensionMessage) { + if (!display) { + return; + } + + const portMessage = { command: "updateOverlayHidden", styles: { display } }; + + this.overlayButtonPort?.postMessage(portMessage); + this.overlayListPort?.postMessage(portMessage); + } + + /** + * Sends a message to the currently active tab to open the autofill overlay. + * + * @param isFocusingFieldElement - Identifies whether the field element should be focused when the overlay is opened + * @param isOpeningFullOverlay - Identifies whether the full overlay should be forced open regardless of other states + */ + private async openOverlay(isFocusingFieldElement = false, isOpeningFullOverlay = false) { + const currentTab = await BrowserApi.getTabFromCurrentWindowId(); + + await BrowserApi.tabSendMessageData(currentTab, "openAutofillOverlay", { + isFocusingFieldElement, + isOpeningFullOverlay, + authStatus: await this.getAuthStatus(), + }); + } + + /** + * Gets the overlay's visibility setting from the settings service. + */ + private async getOverlayVisibility(): Promise { + return await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$); + } + + /** + * Gets the user's authentication status from the auth service. If the user's + * authentication status has changed, the overlay button's authentication status + * will be updated and the overlay list's ciphers will be updated. + */ + private async getAuthStatus() { + const formerAuthStatus = this.userAuthStatus; + this.userAuthStatus = await this.authService.getAuthStatus(); + + if ( + this.userAuthStatus !== formerAuthStatus && + this.userAuthStatus === AuthenticationStatus.Unlocked + ) { + this.updateOverlayButtonAuthStatus(); + await this.updateOverlayCiphers(); + } + + return this.userAuthStatus; + } + + /** + * Sends a message to the overlay button to update its authentication status. + */ + private updateOverlayButtonAuthStatus() { + this.overlayButtonPort?.postMessage({ + command: "updateOverlayButtonAuthStatus", + authStatus: this.userAuthStatus, + }); + } + + /** + * Handles the overlay button being clicked. If the user is not authenticated, + * the vault will be unlocked. If the user is authenticated, the overlay will + * be opened. + * + * @param port - The port of the overlay button + */ + private handleOverlayButtonClicked(port: chrome.runtime.Port) { + if (this.userAuthStatus !== AuthenticationStatus.Unlocked) { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.unlockVault(port); + return; + } + + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.openOverlay(false, true); + } + + /** + * Facilitates opening the unlock popout window. + * + * @param port - The port of the overlay list + */ + private async unlockVault(port: chrome.runtime.Port) { + const { sender } = port; + + this.closeOverlay(port); + const retryMessage: LockedVaultPendingNotificationsData = { + commandToRetry: { message: { command: "openAutofillOverlay" }, sender }, + target: "overlay.background", + }; + await BrowserApi.tabSendMessageData( + sender.tab, + "addToLockedVaultPendingNotifications", + retryMessage, + ); + await this.openUnlockPopout(sender.tab, true); + } + + /** + * Triggers the opening of a vault item popout window associated + * with the passed cipher ID. + * @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID. + * @param sender - The sender of the port message + */ + private async viewSelectedCipher( + { overlayCipherId }: OverlayPortMessage, + { sender }: chrome.runtime.Port, + ) { + const cipher = this.overlayLoginCiphers.get(overlayCipherId); + if (!cipher) { + return; + } + + await this.openViewVaultItemPopout(sender.tab, { + cipherId: cipher.id, + action: SHOW_AUTOFILL_BUTTON, + }); + } + + /** + * Facilitates redirecting focus to the overlay list. + */ + private focusOverlayList() { + this.overlayListPort?.postMessage({ command: "focusOverlayList" }); + } + + /** + * Updates the authentication status for the user and opens the overlay if + * a followup command is present in the message. + * + * @param message - Extension message received from the `unlockCompleted` command + */ + private async unlockCompleted(message: OverlayBackgroundExtensionMessage) { + await this.getAuthStatus(); + + if (message.data?.commandToRetry?.message?.command === "openAutofillOverlay") { + await this.openOverlay(true); + } + } + + /** + * Gets the translations for the overlay page. + */ + private getTranslations() { + if (!this.overlayPageTranslations) { + this.overlayPageTranslations = { + locale: BrowserApi.getUILanguage(), + opensInANewWindow: this.i18nService.translate("opensInANewWindow"), + buttonPageTitle: this.i18nService.translate("bitwardenOverlayButton"), + toggleBitwardenVaultOverlay: this.i18nService.translate("toggleBitwardenVaultOverlay"), + listPageTitle: this.i18nService.translate("bitwardenVault"), + unlockYourAccount: this.i18nService.translate("unlockYourAccountToViewMatchingLogins"), + unlockAccount: this.i18nService.translate("unlockAccount"), + fillCredentialsFor: this.i18nService.translate("fillCredentialsFor"), + partialUsername: this.i18nService.translate("partialUsername"), + view: this.i18nService.translate("view"), + noItemsToShow: this.i18nService.translate("noItemsToShow"), + newItem: this.i18nService.translate("newItem"), + addNewVaultItem: this.i18nService.translate("addNewVaultItem"), + }; + } + + return this.overlayPageTranslations; + } + + /** + * Facilitates redirecting focus out of one of the + * overlay elements to elements on the page. + * + * @param direction - The direction to redirect focus to (either "next", "previous" or "current) + * @param sender - The sender of the port message + */ + private redirectOverlayFocusOut( + { direction }: OverlayPortMessage, + { sender }: chrome.runtime.Port, + ) { + if (!direction) { + return; + } + + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + BrowserApi.tabSendMessageData(sender.tab, "redirectOverlayFocusOut", { direction }); + } + + /** + * Triggers adding a new vault item from the overlay. Gathers data + * input by the user before calling to open the add/edit window. + * + * @param sender - The sender of the port message + */ + private getNewVaultItemDetails({ sender }: chrome.runtime.Port) { + void BrowserApi.tabSendMessage(sender.tab, { command: "addNewVaultItemFromOverlay" }); + } + + /** + * Handles adding a new vault item from the overlay. Gathers data login + * data captured in the extension message. + * + * @param login - The login data captured from the extension message + * @param sender - The sender of the extension message + */ + private async addNewVaultItem( + { login }: OverlayAddNewItemMessage, + sender: chrome.runtime.MessageSender, + ) { + if (!login) { + return; + } + + const uriView = new LoginUriView(); + uriView.uri = login.uri; + + const loginView = new LoginView(); + loginView.uris = [uriView]; + loginView.username = login.username || ""; + loginView.password = login.password || ""; + + const cipherView = new CipherView(); + cipherView.name = (Utils.getHostname(login.uri) || login.hostname).replace(/^www\./, ""); + cipherView.folderId = null; + cipherView.type = CipherType.Login; + cipherView.login = loginView; + + await this.cipherService.setAddEditCipherInfo({ + cipher: cipherView, + collectionIds: cipherView.collectionIds, + }); + + await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id }); + await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher"); + } + + /** + * Sets up the extension message listeners for the overlay. + */ + private setupExtensionMessageListeners() { + BrowserApi.messageListener("overlay.background", this.handleExtensionMessage); + BrowserApi.addListener(chrome.runtime.onConnect, this.handlePortOnConnect); + } + + /** + * Handles extension messages sent to the extension background. + * + * @param message - The message received from the extension + * @param sender - The sender of the message + * @param sendResponse - The response to send back to the sender + */ + private handleExtensionMessage = ( + message: OverlayBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + sendResponse: (response?: any) => void, + ) => { + const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command]; + if (!handler) { + return null; + } + + const messageResponse = handler({ message, sender }); + if (typeof messageResponse === "undefined") { + return null; + } + + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + Promise.resolve(messageResponse).then((response) => sendResponse(response)); + return true; + }; + + /** + * Handles the connection of a port to the extension background. + * + * @param port - The port that connected to the extension background + */ + private handlePortOnConnect = async (port: chrome.runtime.Port) => { + const isOverlayListPort = port.name === AutofillOverlayPort.List; + const isOverlayButtonPort = port.name === AutofillOverlayPort.Button; + if (!isOverlayListPort && !isOverlayButtonPort) { + return; + } + + this.storeOverlayPort(port); + port.onMessage.addListener(this.handleOverlayElementPortMessage); + port.postMessage({ + command: `initAutofillOverlay${isOverlayListPort ? "List" : "Button"}`, + authStatus: await this.getAuthStatus(), + styleSheetUrl: chrome.runtime.getURL(`overlay/${isOverlayListPort ? "list" : "button"}.css`), + theme: await firstValueFrom(this.themeStateService.selectedTheme$), + translations: this.getTranslations(), + ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null, + }); + this.updateOverlayPosition( + { + overlayElement: isOverlayListPort + ? AutofillOverlayElement.List + : AutofillOverlayElement.Button, + }, + port.sender, + ); + }; + + /** + * Stores the connected overlay port and sets up any existing ports to be disconnected. + * + * @param port - The port to store +| */ + private storeOverlayPort(port: chrome.runtime.Port) { + if (port.name === AutofillOverlayPort.List) { + this.storeExpiredOverlayPort(this.overlayListPort); + this.overlayListPort = port; + return; + } + + if (port.name === AutofillOverlayPort.Button) { + this.storeExpiredOverlayPort(this.overlayButtonPort); + this.overlayButtonPort = port; + } + } + + /** + * When registering a new connection, we want to ensure that the port is disconnected. + * This method places an existing port in the expiredPorts array to be disconnected + * at a later time. + * + * @param port - The port to store in the expiredPorts array + */ + private storeExpiredOverlayPort(port: chrome.runtime.Port | null) { + if (port) { + this.expiredPorts.push(port); + } + } + + /** + * Handles messages sent to the overlay list or button ports. + * + * @param message - The message received from the port + * @param port - The port that sent the message + */ + private handleOverlayElementPortMessage = ( + message: OverlayBackgroundExtensionMessage, + port: chrome.runtime.Port, + ) => { + const command = message?.command; + let handler: CallableFunction | undefined; + + if (port.name === AutofillOverlayPort.Button) { + handler = this.overlayButtonPortMessageHandlers[command]; + } + + if (port.name === AutofillOverlayPort.List) { + handler = this.overlayListPortMessageHandlers[command]; + } + + if (!handler) { + return; + } + + handler({ message, port }); + }; +} + +export default LegacyOverlayBackground; diff --git a/apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts b/apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts new file mode 100644 index 00000000000..ed422822b36 --- /dev/null +++ b/apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts @@ -0,0 +1,41 @@ +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; + +import AutofillScript from "../../../models/autofill-script"; + +type AutofillExtensionMessage = { + command: string; + tab?: chrome.tabs.Tab; + sender?: string; + fillScript?: AutofillScript; + url?: string; + pageDetailsUrl?: string; + ciphers?: any; + data?: { + authStatus?: AuthenticationStatus; + isFocusingFieldElement?: boolean; + isOverlayCiphersPopulated?: boolean; + direction?: "previous" | "next"; + isOpeningFullOverlay?: boolean; + forceCloseOverlay?: boolean; + autofillOverlayVisibility?: number; + }; +}; + +type AutofillExtensionMessageParam = { message: AutofillExtensionMessage }; + +type AutofillExtensionMessageHandlers = { + [key: string]: CallableFunction; + collectPageDetails: ({ message }: AutofillExtensionMessageParam) => void; + collectPageDetailsImmediately: ({ message }: AutofillExtensionMessageParam) => void; + fillForm: ({ message }: AutofillExtensionMessageParam) => void; + openAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void; + closeAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void; + addNewVaultItemFromOverlay: () => void; + redirectOverlayFocusOut: ({ message }: AutofillExtensionMessageParam) => void; + updateIsOverlayCiphersPopulated: ({ message }: AutofillExtensionMessageParam) => void; + bgUnlockPopoutOpened: () => void; + bgVaultItemRepromptPopoutOpened: () => void; + updateAutofillOverlayVisibility: ({ message }: AutofillExtensionMessageParam) => void; +}; + +export { AutofillExtensionMessage, AutofillExtensionMessageHandlers }; diff --git a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts new file mode 100644 index 00000000000..96d5e85ca34 --- /dev/null +++ b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts @@ -0,0 +1,604 @@ +import { mock } from "jest-mock-extended"; + +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; + +import { RedirectFocusDirection } from "../../enums/autofill-overlay.enum"; +import AutofillPageDetails from "../../models/autofill-page-details"; +import AutofillScript from "../../models/autofill-script"; +import { + flushPromises, + mockQuerySelectorAllDefinedCall, + sendMockExtensionMessage, +} from "../../spec/testing-utils"; +import AutofillOverlayContentServiceDeprecated from "../services/autofill-overlay-content.service.deprecated"; + +import { AutofillExtensionMessage } from "./abstractions/autofill-init.deprecated"; +import AutofillInitDeprecated from "./autofill-init.deprecated"; + +describe("AutofillInit", () => { + let autofillInit: AutofillInitDeprecated; + const autofillOverlayContentService = mock(); + const originalDocumentReadyState = document.readyState; + const mockQuerySelectorAll = mockQuerySelectorAllDefinedCall(); + + beforeEach(() => { + chrome.runtime.connect = jest.fn().mockReturnValue({ + onDisconnect: { + addListener: jest.fn(), + }, + }); + autofillInit = new AutofillInitDeprecated(autofillOverlayContentService); + window.IntersectionObserver = jest.fn(() => mock()); + }); + + afterEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + Object.defineProperty(document, "readyState", { + value: originalDocumentReadyState, + writable: true, + }); + }); + + afterAll(() => { + mockQuerySelectorAll.mockRestore(); + }); + + describe("init", () => { + it("sets up the extension message listeners", () => { + jest.spyOn(autofillInit as any, "setupExtensionMessageListeners"); + + autofillInit.init(); + + expect(autofillInit["setupExtensionMessageListeners"]).toHaveBeenCalled(); + }); + + it("triggers a collection of page details if the document is in a `complete` ready state", () => { + jest.useFakeTimers(); + Object.defineProperty(document, "readyState", { value: "complete", writable: true }); + + autofillInit.init(); + jest.advanceTimersByTime(250); + + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith( + { + command: "bgCollectPageDetails", + sender: "autofillInit", + }, + expect.any(Function), + ); + }); + + it("registers a window load listener to collect the page details if the document is not in a `complete` ready state", () => { + jest.spyOn(window, "addEventListener"); + Object.defineProperty(document, "readyState", { value: "loading", writable: true }); + + autofillInit.init(); + + expect(window.addEventListener).toHaveBeenCalledWith("load", expect.any(Function)); + }); + }); + + describe("setupExtensionMessageListeners", () => { + it("sets up a chrome runtime on message listener", () => { + jest.spyOn(chrome.runtime.onMessage, "addListener"); + + autofillInit["setupExtensionMessageListeners"](); + + expect(chrome.runtime.onMessage.addListener).toHaveBeenCalledWith( + autofillInit["handleExtensionMessage"], + ); + }); + }); + + describe("handleExtensionMessage", () => { + let message: AutofillExtensionMessage; + let sender: chrome.runtime.MessageSender; + const sendResponse = jest.fn(); + + beforeEach(() => { + message = { + command: "collectPageDetails", + tab: mock(), + sender: "sender", + }; + sender = mock(); + }); + + it("returns a undefined value if a extension message handler is not found with the given message command", () => { + message.command = "unknownCommand"; + + const response = autofillInit["handleExtensionMessage"](message, sender, sendResponse); + + expect(response).toBe(null); + }); + + it("returns a undefined value if the message handler does not return a response", async () => { + const response1 = await autofillInit["handleExtensionMessage"](message, sender, sendResponse); + await flushPromises(); + + expect(response1).not.toBe(false); + + message.command = "removeAutofillOverlay"; + message.fillScript = mock(); + + const response2 = autofillInit["handleExtensionMessage"](message, sender, sendResponse); + await flushPromises(); + + expect(response2).toBe(null); + }); + + it("returns a true value and calls sendResponse if the message handler returns a response", async () => { + message.command = "collectPageDetailsImmediately"; + const pageDetails: AutofillPageDetails = { + title: "title", + url: "http://example.com", + documentUrl: "documentUrl", + forms: {}, + fields: [], + collectedTimestamp: 0, + }; + jest + .spyOn(autofillInit["collectAutofillContentService"], "getPageDetails") + .mockResolvedValue(pageDetails); + + const response = await autofillInit["handleExtensionMessage"](message, sender, sendResponse); + await flushPromises(); + + expect(response).toBe(true); + expect(sendResponse).toHaveBeenCalledWith(pageDetails); + }); + + describe("extension message handlers", () => { + beforeEach(() => { + autofillInit.init(); + }); + + describe("collectPageDetails", () => { + it("sends the collected page details for autofill using a background script message", async () => { + const pageDetails: AutofillPageDetails = { + title: "title", + url: "http://example.com", + documentUrl: "documentUrl", + forms: {}, + fields: [], + collectedTimestamp: 0, + }; + const message = { + command: "collectPageDetails", + sender: "sender", + tab: mock(), + }; + jest + .spyOn(autofillInit["collectAutofillContentService"], "getPageDetails") + .mockResolvedValue(pageDetails); + + sendMockExtensionMessage(message, sender, sendResponse); + await flushPromises(); + + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ + command: "collectPageDetailsResponse", + tab: message.tab, + details: pageDetails, + sender: message.sender, + }); + }); + }); + + describe("collectPageDetailsImmediately", () => { + it("returns collected page details for autofill if set to send the details in the response", async () => { + const pageDetails: AutofillPageDetails = { + title: "title", + url: "http://example.com", + documentUrl: "documentUrl", + forms: {}, + fields: [], + collectedTimestamp: 0, + }; + jest + .spyOn(autofillInit["collectAutofillContentService"], "getPageDetails") + .mockResolvedValue(pageDetails); + + sendMockExtensionMessage( + { command: "collectPageDetailsImmediately" }, + sender, + sendResponse, + ); + await flushPromises(); + + expect(autofillInit["collectAutofillContentService"].getPageDetails).toHaveBeenCalled(); + expect(sendResponse).toBeCalledWith(pageDetails); + expect(chrome.runtime.sendMessage).not.toHaveBeenCalledWith({ + command: "collectPageDetailsResponse", + tab: message.tab, + details: pageDetails, + sender: message.sender, + }); + }); + }); + + describe("fillForm", () => { + let fillScript: AutofillScript; + beforeEach(() => { + fillScript = mock(); + jest.spyOn(autofillInit["insertAutofillContentService"], "fillForm").mockImplementation(); + }); + + it("skips calling the InsertAutofillContentService and does not fill the form if the url to fill is not equal to the current tab url", async () => { + const fillScript = mock(); + const message = { + command: "fillForm", + fillScript, + pageDetailsUrl: "https://a-different-url.com", + }; + + sendMockExtensionMessage(message); + await flushPromises(); + + expect(autofillInit["insertAutofillContentService"].fillForm).not.toHaveBeenCalledWith( + fillScript, + ); + }); + + it("calls the InsertAutofillContentService to fill the form", async () => { + sendMockExtensionMessage({ + command: "fillForm", + fillScript, + pageDetailsUrl: window.location.href, + }); + await flushPromises(); + + expect(autofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( + fillScript, + ); + }); + + it("removes the overlay when filling the form", async () => { + const blurAndRemoveOverlaySpy = jest.spyOn(autofillInit as any, "blurAndRemoveOverlay"); + sendMockExtensionMessage({ + command: "fillForm", + fillScript, + pageDetailsUrl: window.location.href, + }); + await flushPromises(); + + expect(blurAndRemoveOverlaySpy).toHaveBeenCalled(); + }); + + it("updates the isCurrentlyFilling property of the overlay to true after filling", async () => { + jest.useFakeTimers(); + jest.spyOn(autofillInit as any, "updateOverlayIsCurrentlyFilling"); + jest + .spyOn(autofillInit["autofillOverlayContentService"], "focusMostRecentOverlayField") + .mockImplementation(); + + sendMockExtensionMessage({ + command: "fillForm", + fillScript, + pageDetailsUrl: window.location.href, + }); + await flushPromises(); + jest.advanceTimersByTime(300); + + expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(1, true); + expect(autofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( + fillScript, + ); + expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(2, false); + }); + + it("skips attempting to focus the most recent field if the autofillOverlayContentService is not present", async () => { + jest.useFakeTimers(); + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + jest.spyOn(newAutofillInit as any, "updateOverlayIsCurrentlyFilling"); + jest + .spyOn(newAutofillInit["insertAutofillContentService"], "fillForm") + .mockImplementation(); + + sendMockExtensionMessage({ + command: "fillForm", + fillScript, + pageDetailsUrl: window.location.href, + }); + await flushPromises(); + jest.advanceTimersByTime(300); + + expect(newAutofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith( + 1, + true, + ); + expect(newAutofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( + fillScript, + ); + expect(newAutofillInit["updateOverlayIsCurrentlyFilling"]).not.toHaveBeenNthCalledWith( + 2, + false, + ); + }); + }); + + describe("openAutofillOverlay", () => { + const message = { + command: "openAutofillOverlay", + data: { + isFocusingFieldElement: true, + isOpeningFullOverlay: true, + authStatus: AuthenticationStatus.Unlocked, + }, + }; + + it("skips attempting to open the autofill overlay if the autofillOverlayContentService is not present", () => { + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + + sendMockExtensionMessage(message); + + expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); + }); + + it("opens the autofill overlay", () => { + sendMockExtensionMessage(message); + + expect( + autofillInit["autofillOverlayContentService"].openAutofillOverlay, + ).toHaveBeenCalledWith({ + isFocusingFieldElement: message.data.isFocusingFieldElement, + isOpeningFullOverlay: message.data.isOpeningFullOverlay, + authStatus: message.data.authStatus, + }); + }); + }); + + describe("closeAutofillOverlay", () => { + beforeEach(() => { + autofillInit["autofillOverlayContentService"].isFieldCurrentlyFocused = false; + autofillInit["autofillOverlayContentService"].isCurrentlyFilling = false; + }); + + it("skips attempting to remove the overlay if the autofillOverlayContentService is not present", () => { + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); + + sendMockExtensionMessage({ + command: "closeAutofillOverlay", + data: { forceCloseOverlay: false }, + }); + + expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); + }); + + it("removes the autofill overlay if the message flags a forced closure", () => { + sendMockExtensionMessage({ + command: "closeAutofillOverlay", + data: { forceCloseOverlay: true }, + }); + + expect( + autofillInit["autofillOverlayContentService"].removeAutofillOverlay, + ).toHaveBeenCalled(); + }); + + it("ignores the message if a field is currently focused", () => { + autofillInit["autofillOverlayContentService"].isFieldCurrentlyFocused = true; + + sendMockExtensionMessage({ command: "closeAutofillOverlay" }); + + expect( + autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, + ).not.toHaveBeenCalled(); + expect( + autofillInit["autofillOverlayContentService"].removeAutofillOverlay, + ).not.toHaveBeenCalled(); + }); + + it("removes the autofill overlay list if the overlay is currently filling", () => { + autofillInit["autofillOverlayContentService"].isCurrentlyFilling = true; + + sendMockExtensionMessage({ command: "closeAutofillOverlay" }); + + expect( + autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, + ).toHaveBeenCalled(); + expect( + autofillInit["autofillOverlayContentService"].removeAutofillOverlay, + ).not.toHaveBeenCalled(); + }); + + it("removes the entire overlay if the overlay is not currently filling", () => { + sendMockExtensionMessage({ command: "closeAutofillOverlay" }); + + expect( + autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, + ).not.toHaveBeenCalled(); + expect( + autofillInit["autofillOverlayContentService"].removeAutofillOverlay, + ).toHaveBeenCalled(); + }); + }); + + describe("addNewVaultItemFromOverlay", () => { + it("will not add a new vault item if the autofillOverlayContentService is not present", () => { + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + + sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" }); + + expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); + }); + + it("will add a new vault item", () => { + sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" }); + + expect(autofillInit["autofillOverlayContentService"].addNewVaultItem).toHaveBeenCalled(); + }); + }); + + describe("redirectOverlayFocusOut", () => { + const message = { + command: "redirectOverlayFocusOut", + data: { + direction: RedirectFocusDirection.Next, + }, + }; + + it("ignores the message to redirect focus if the autofillOverlayContentService does not exist", () => { + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + + sendMockExtensionMessage(message); + + expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); + }); + + it("redirects the overlay focus", () => { + sendMockExtensionMessage(message); + + expect( + autofillInit["autofillOverlayContentService"].redirectOverlayFocusOut, + ).toHaveBeenCalledWith(message.data.direction); + }); + }); + + describe("updateIsOverlayCiphersPopulated", () => { + const message = { + command: "updateIsOverlayCiphersPopulated", + data: { + isOverlayCiphersPopulated: true, + }, + }; + + it("skips updating whether the ciphers are populated if the autofillOverlayContentService does note exist", () => { + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + + sendMockExtensionMessage(message); + + expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); + }); + + it("updates whether the overlay ciphers are populated", () => { + sendMockExtensionMessage(message); + + expect(autofillInit["autofillOverlayContentService"].isOverlayCiphersPopulated).toEqual( + message.data.isOverlayCiphersPopulated, + ); + }); + }); + + describe("bgUnlockPopoutOpened", () => { + it("skips attempting to blur and remove the overlay if the autofillOverlayContentService is not present", () => { + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); + + sendMockExtensionMessage({ command: "bgUnlockPopoutOpened" }); + + expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); + expect(newAutofillInit["removeAutofillOverlay"]).not.toHaveBeenCalled(); + }); + + it("blurs the most recently focused feel and remove the autofill overlay", () => { + jest.spyOn(autofillInit["autofillOverlayContentService"], "blurMostRecentOverlayField"); + jest.spyOn(autofillInit as any, "removeAutofillOverlay"); + + sendMockExtensionMessage({ command: "bgUnlockPopoutOpened" }); + + expect( + autofillInit["autofillOverlayContentService"].blurMostRecentOverlayField, + ).toHaveBeenCalled(); + expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled(); + }); + }); + + describe("bgVaultItemRepromptPopoutOpened", () => { + it("skips attempting to blur and remove the overlay if the autofillOverlayContentService is not present", () => { + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); + + sendMockExtensionMessage({ command: "bgVaultItemRepromptPopoutOpened" }); + + expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); + expect(newAutofillInit["removeAutofillOverlay"]).not.toHaveBeenCalled(); + }); + + it("blurs the most recently focused feel and remove the autofill overlay", () => { + jest.spyOn(autofillInit["autofillOverlayContentService"], "blurMostRecentOverlayField"); + jest.spyOn(autofillInit as any, "removeAutofillOverlay"); + + sendMockExtensionMessage({ command: "bgVaultItemRepromptPopoutOpened" }); + + expect( + autofillInit["autofillOverlayContentService"].blurMostRecentOverlayField, + ).toHaveBeenCalled(); + expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled(); + }); + }); + + describe("updateAutofillOverlayVisibility", () => { + beforeEach(() => { + autofillInit["autofillOverlayContentService"].autofillOverlayVisibility = + AutofillOverlayVisibility.OnButtonClick; + }); + + it("skips attempting to update the overlay visibility if the autofillOverlayVisibility data value is not present", () => { + sendMockExtensionMessage({ + command: "updateAutofillOverlayVisibility", + data: {}, + }); + + expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual( + AutofillOverlayVisibility.OnButtonClick, + ); + }); + + it("updates the overlay visibility value", () => { + const message = { + command: "updateAutofillOverlayVisibility", + data: { + autofillOverlayVisibility: AutofillOverlayVisibility.Off, + }, + }; + + sendMockExtensionMessage(message); + + expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual( + message.data.autofillOverlayVisibility, + ); + }); + }); + }); + }); + + describe("destroy", () => { + it("clears the timeout used to collect page details on load", () => { + jest.spyOn(window, "clearTimeout"); + + autofillInit.init(); + autofillInit.destroy(); + + expect(window.clearTimeout).toHaveBeenCalledWith( + autofillInit["collectPageDetailsOnLoadTimeout"], + ); + }); + + it("removes the extension message listeners", () => { + autofillInit.destroy(); + + expect(chrome.runtime.onMessage.removeListener).toHaveBeenCalledWith( + autofillInit["handleExtensionMessage"], + ); + }); + + it("destroys the collectAutofillContentService", () => { + jest.spyOn(autofillInit["collectAutofillContentService"], "destroy"); + + autofillInit.destroy(); + + expect(autofillInit["collectAutofillContentService"].destroy).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts new file mode 100644 index 00000000000..3e36fa43bbd --- /dev/null +++ b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts @@ -0,0 +1,310 @@ +import { AutofillInit } from "../../content/abstractions/autofill-init"; +import AutofillPageDetails from "../../models/autofill-page-details"; +import CollectAutofillContentService from "../../services/collect-autofill-content.service"; +import DomElementVisibilityService from "../../services/dom-element-visibility.service"; +import InsertAutofillContentService from "../../services/insert-autofill-content.service"; +import { sendExtensionMessage } from "../../utils"; +import { LegacyAutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service"; + +import { + AutofillExtensionMessage, + AutofillExtensionMessageHandlers, +} from "./abstractions/autofill-init.deprecated"; + +class LegacyAutofillInit implements AutofillInit { + private readonly autofillOverlayContentService: LegacyAutofillOverlayContentService | undefined; + private readonly domElementVisibilityService: DomElementVisibilityService; + private readonly collectAutofillContentService: CollectAutofillContentService; + private readonly insertAutofillContentService: InsertAutofillContentService; + private collectPageDetailsOnLoadTimeout: number | NodeJS.Timeout | undefined; + private readonly extensionMessageHandlers: AutofillExtensionMessageHandlers = { + collectPageDetails: ({ message }) => this.collectPageDetails(message), + collectPageDetailsImmediately: ({ message }) => this.collectPageDetails(message, true), + fillForm: ({ message }) => this.fillForm(message), + openAutofillOverlay: ({ message }) => this.openAutofillOverlay(message), + closeAutofillOverlay: ({ message }) => this.removeAutofillOverlay(message), + addNewVaultItemFromOverlay: () => this.addNewVaultItemFromOverlay(), + redirectOverlayFocusOut: ({ message }) => this.redirectOverlayFocusOut(message), + updateIsOverlayCiphersPopulated: ({ message }) => this.updateIsOverlayCiphersPopulated(message), + bgUnlockPopoutOpened: () => this.blurAndRemoveOverlay(), + bgVaultItemRepromptPopoutOpened: () => this.blurAndRemoveOverlay(), + updateAutofillOverlayVisibility: ({ message }) => this.updateAutofillOverlayVisibility(message), + }; + + /** + * AutofillInit constructor. Initializes the DomElementVisibilityService, + * CollectAutofillContentService and InsertAutofillContentService classes. + * + * @param autofillOverlayContentService - The autofill overlay content service, potentially undefined. + */ + constructor(autofillOverlayContentService?: LegacyAutofillOverlayContentService) { + this.autofillOverlayContentService = autofillOverlayContentService; + this.domElementVisibilityService = new DomElementVisibilityService(); + this.collectAutofillContentService = new CollectAutofillContentService( + this.domElementVisibilityService, + this.autofillOverlayContentService, + ); + this.insertAutofillContentService = new InsertAutofillContentService( + this.domElementVisibilityService, + this.collectAutofillContentService, + ); + } + + /** + * Initializes the autofill content script, setting up + * the extension message listeners. This method should + * be called once when the content script is loaded. + */ + init() { + this.setupExtensionMessageListeners(); + this.autofillOverlayContentService?.init(); + this.collectPageDetailsOnLoad(); + } + + /** + * Triggers a collection of the page details from the + * background script, ensuring that autofill is ready + * to act on the page. + */ + private collectPageDetailsOnLoad() { + const sendCollectDetailsMessage = () => { + this.clearCollectPageDetailsOnLoadTimeout(); + this.collectPageDetailsOnLoadTimeout = setTimeout( + () => sendExtensionMessage("bgCollectPageDetails", { sender: "autofillInit" }), + 250, + ); + }; + + if (globalThis.document.readyState === "complete") { + sendCollectDetailsMessage(); + } + + globalThis.addEventListener("load", sendCollectDetailsMessage); + } + + /** + * Collects the page details and sends them to the + * extension background script. If the `sendDetailsInResponse` + * parameter is set to true, the page details will be + * returned to facilitate sending the details in the + * response to the extension message. + * + * @param message - The extension message. + * @param sendDetailsInResponse - Determines whether to send the details in the response. + */ + private async collectPageDetails( + message: AutofillExtensionMessage, + sendDetailsInResponse = false, + ): Promise { + const pageDetails: AutofillPageDetails = + await this.collectAutofillContentService.getPageDetails(); + if (sendDetailsInResponse) { + return pageDetails; + } + + void chrome.runtime.sendMessage({ + command: "collectPageDetailsResponse", + tab: message.tab, + details: pageDetails, + sender: message.sender, + }); + } + + /** + * Fills the form with the given fill script. + * + * @param {AutofillExtensionMessage} message + */ + private async fillForm({ fillScript, pageDetailsUrl }: AutofillExtensionMessage) { + if ((document.defaultView || window).location.href !== pageDetailsUrl) { + return; + } + + this.blurAndRemoveOverlay(); + this.updateOverlayIsCurrentlyFilling(true); + await this.insertAutofillContentService.fillForm(fillScript); + + if (!this.autofillOverlayContentService) { + return; + } + + setTimeout(() => this.updateOverlayIsCurrentlyFilling(false), 250); + } + + /** + * Handles updating the overlay is currently filling value. + * + * @param isCurrentlyFilling - Indicates if the overlay is currently filling + */ + private updateOverlayIsCurrentlyFilling(isCurrentlyFilling: boolean) { + if (!this.autofillOverlayContentService) { + return; + } + + this.autofillOverlayContentService.isCurrentlyFilling = isCurrentlyFilling; + } + + /** + * Opens the autofill overlay. + * + * @param data - The extension message data. + */ + private openAutofillOverlay({ data }: AutofillExtensionMessage) { + if (!this.autofillOverlayContentService) { + return; + } + + this.autofillOverlayContentService.openAutofillOverlay(data); + } + + /** + * Blurs the most recent overlay field and removes the overlay. Used + * in cases where the background unlock or vault item reprompt popout + * is opened. + */ + private blurAndRemoveOverlay() { + if (!this.autofillOverlayContentService) { + return; + } + + this.autofillOverlayContentService.blurMostRecentOverlayField(); + this.removeAutofillOverlay(); + } + + /** + * Removes the autofill overlay if the field is not currently focused. + * If the autofill is currently filling, only the overlay list will be + * removed. + */ + private removeAutofillOverlay(message?: AutofillExtensionMessage) { + if (message?.data?.forceCloseOverlay) { + this.autofillOverlayContentService?.removeAutofillOverlay(); + return; + } + + if ( + !this.autofillOverlayContentService || + this.autofillOverlayContentService.isFieldCurrentlyFocused + ) { + return; + } + + if (this.autofillOverlayContentService.isCurrentlyFilling) { + this.autofillOverlayContentService.removeAutofillOverlayList(); + return; + } + + this.autofillOverlayContentService.removeAutofillOverlay(); + } + + /** + * Adds a new vault item from the overlay. + */ + private addNewVaultItemFromOverlay() { + if (!this.autofillOverlayContentService) { + return; + } + + this.autofillOverlayContentService.addNewVaultItem(); + } + + /** + * Redirects the overlay focus out of an overlay iframe. + * + * @param data - Contains the direction to redirect the focus. + */ + private redirectOverlayFocusOut({ data }: AutofillExtensionMessage) { + if (!this.autofillOverlayContentService) { + return; + } + + this.autofillOverlayContentService.redirectOverlayFocusOut(data?.direction); + } + + /** + * Updates whether the current tab has ciphers that can populate the overlay list + * + * @param data - Contains the isOverlayCiphersPopulated value + * + */ + private updateIsOverlayCiphersPopulated({ data }: AutofillExtensionMessage) { + if (!this.autofillOverlayContentService) { + return; + } + + this.autofillOverlayContentService.isOverlayCiphersPopulated = Boolean( + data?.isOverlayCiphersPopulated, + ); + } + + /** + * Updates the autofill overlay visibility. + * + * @param data - Contains the autoFillOverlayVisibility value + */ + private updateAutofillOverlayVisibility({ data }: AutofillExtensionMessage) { + if (!this.autofillOverlayContentService || isNaN(data?.autofillOverlayVisibility)) { + return; + } + + this.autofillOverlayContentService.autofillOverlayVisibility = data?.autofillOverlayVisibility; + } + + /** + * Clears the send collect details message timeout. + */ + private clearCollectPageDetailsOnLoadTimeout() { + if (this.collectPageDetailsOnLoadTimeout) { + clearTimeout(this.collectPageDetailsOnLoadTimeout); + } + } + + /** + * Sets up the extension message listeners for the content script. + */ + private setupExtensionMessageListeners() { + chrome.runtime.onMessage.addListener(this.handleExtensionMessage); + } + + /** + * Handles the extension messages sent to the content script. + * + * @param message - The extension message. + * @param sender - The message sender. + * @param sendResponse - The send response callback. + */ + private handleExtensionMessage = ( + message: AutofillExtensionMessage, + sender: chrome.runtime.MessageSender, + sendResponse: (response?: any) => void, + ): boolean => { + const command: string = message.command; + const handler: CallableFunction | undefined = this.extensionMessageHandlers[command]; + if (!handler) { + return null; + } + + const messageResponse = handler({ message, sender }); + if (typeof messageResponse === "undefined") { + return null; + } + + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + Promise.resolve(messageResponse).then((response) => sendResponse(response)); + return true; + }; + + /** + * Handles destroying the autofill init content script. Removes all + * listeners, timeouts, and object instances to prevent memory leaks. + */ + destroy() { + this.clearCollectPageDetailsOnLoadTimeout(); + chrome.runtime.onMessage.removeListener(this.handleExtensionMessage); + this.collectAutofillContentService.destroy(); + this.autofillOverlayContentService?.destroy(); + } +} + +export default LegacyAutofillInit; diff --git a/apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts b/apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts new file mode 100644 index 00000000000..66d672172ae --- /dev/null +++ b/apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts @@ -0,0 +1,14 @@ +import { setupAutofillInitDisconnectAction } from "../../utils"; +import LegacyAutofillOverlayContentService from "../services/autofill-overlay-content.service.deprecated"; + +import LegacyAutofillInit from "./autofill-init.deprecated"; + +(function (windowContext) { + if (!windowContext.bitwardenAutofillInit) { + const autofillOverlayContentService = new LegacyAutofillOverlayContentService(); + windowContext.bitwardenAutofillInit = new LegacyAutofillInit(autofillOverlayContentService); + setupAutofillInitDisconnectAction(windowContext); + + windowContext.bitwardenAutofillInit.init(); + } +})(window); diff --git a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-button.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-button.deprecated.ts similarity index 100% rename from apps/browser/src/autofill/overlay/abstractions/autofill-overlay-button.ts rename to apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-button.deprecated.ts diff --git a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-iframe.service.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-iframe.service.deprecated.ts similarity index 100% rename from apps/browser/src/autofill/overlay/abstractions/autofill-overlay-iframe.service.ts rename to apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-iframe.service.deprecated.ts diff --git a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-list.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts similarity index 96% rename from apps/browser/src/autofill/overlay/abstractions/autofill-overlay-list.ts rename to apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts index b656f238dce..83578b13043 100644 --- a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-list.ts +++ b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts @@ -1,6 +1,6 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { OverlayCipherData } from "../../background/abstractions/overlay.background"; +import { OverlayCipherData } from "../../background/abstractions/overlay.background.deprecated"; type OverlayListMessage = { command: string }; diff --git a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-page-element.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts similarity index 89% rename from apps/browser/src/autofill/overlay/abstractions/autofill-overlay-page-element.ts rename to apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts index eb3c2fa4a71..368ae4e7303 100644 --- a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-page-element.ts +++ b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts @@ -1,5 +1,5 @@ -import { OverlayButtonWindowMessageHandlers } from "./autofill-overlay-button"; -import { OverlayListWindowMessageHandlers } from "./autofill-overlay-list"; +import { OverlayButtonWindowMessageHandlers } from "./autofill-overlay-button.deprecated"; +import { OverlayListWindowMessageHandlers } from "./autofill-overlay-list.deprecated"; type WindowMessageHandlers = OverlayButtonWindowMessageHandlers | OverlayListWindowMessageHandlers; diff --git a/apps/browser/src/autofill/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.spec.ts.snap b/apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap similarity index 95% rename from apps/browser/src/autofill/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.spec.ts.snap rename to apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap index cb8e4a541bb..132bd968899 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.spec.ts.snap +++ b/apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap @@ -15,7 +15,7 @@ exports[`AutofillOverlayIframeService initOverlayIframe sets up the iframe's att `; + }); + + it("returns null if the sub frame URL cannot be parsed correctly", async () => { + delete globalThis.location; + globalThis.location = { href: "invalid-base" } as Location; + sendMockExtensionMessage( + { + command: "getSubFrameOffsets", + subFrameUrl: iframeSource, + }, + mock(), + sendResponseSpy, + ); + await flushPromises(); + + expect(sendResponseSpy).toHaveBeenCalledWith(null); + }); + + it("calculates the sub frame's offsets if a single frame with the referenced url exists", async () => { + sendMockExtensionMessage( + { + command: "getSubFrameOffsets", + subFrameUrl: iframeSource, + }, + mock(), + sendResponseSpy, + ); + await flushPromises(); + + expect(sendResponseSpy).toHaveBeenCalledWith({ + frameId: undefined, + left: 2, + top: 2, + url: iframeSource, + }); + }); + + it("returns null if a matching iframe is not found", async () => { + document.body.innerHTML = ""; + sendMockExtensionMessage( + { + command: "getSubFrameOffsets", + subFrameUrl: iframeSource, + }, + mock(), + sendResponseSpy, + ); + await flushPromises(); + + expect(sendResponseSpy).toHaveBeenCalledWith(null); + }); + + it("returns null if two or more iframes are found with the same src", async () => { + document.body.innerHTML = ` + + + `; + + sendMockExtensionMessage( + { + command: "getSubFrameOffsets", + subFrameUrl: iframeSource, + }, + mock(), + sendResponseSpy, + ); + await flushPromises(); + + expect(sendResponseSpy).toHaveBeenCalledWith(null); + }); + }); + + describe("getSubFrameOffsetsFromWindowMessage", () => { + it("sends a message to the parent to calculate the sub frame positioning", () => { + jest.spyOn(globalThis.parent, "postMessage").mockImplementation(); + const subFrameId = 10; + + sendMockExtensionMessage({ + command: "getSubFrameOffsetsFromWindowMessage", + subFrameId, + }); + + expect(globalThis.parent.postMessage).toHaveBeenCalledWith( + { + command: "calculateSubFramePositioning", + subFrameData: { + url: window.location.href, + frameId: subFrameId, + left: 0, + top: 0, + parentFrameIds: [0], + subFrameDepth: 0, + }, + }, + "*", + ); + }); + + describe("calculateSubFramePositioning", () => { + beforeEach(() => { + autofillOverlayContentService.init(); + jest.spyOn(globalThis.parent, "postMessage"); + document.body.innerHTML = ``; + }); + + it("destroys the inline menu listeners on the origin frame if the depth exceeds the threshold", async () => { + document.body.innerHTML = ``; + const iframe = document.querySelector("iframe") as HTMLIFrameElement; + const subFrameData = { + url: "https://example.com/", + frameId: 10, + left: 0, + top: 0, + parentFrameIds: [1, 2, 3], + subFrameDepth: MAX_SUB_FRAME_DEPTH, + }; + sendExtensionMessageSpy.mockResolvedValue(4); + + postWindowMessage( + { command: "calculateSubFramePositioning", subFrameData }, + "*", + iframe.contentWindow as any, + ); + await flushPromises(); + + expect(globalThis.parent.postMessage).not.toHaveBeenCalled(); + }); + + it("calculates the sub frame offset for the current frame and sends those values to the parent if not in the top frame", async () => { + Object.defineProperty(window, "top", { + value: null, + writable: true, + }); + document.body.innerHTML = ``; + const iframe = document.querySelector("iframe") as HTMLIFrameElement; + const subFrameData = { + url: "https://example.com/", + frameId: 10, + left: 0, + top: 0, + parentFrameIds: [1, 2, 3], + subFrameDepth: 0, + }; + + postWindowMessage( + { command: "calculateSubFramePositioning", subFrameData }, + "*", + iframe.contentWindow as any, + ); + await flushPromises(); + + expect(globalThis.parent.postMessage).toHaveBeenCalledWith( + { + command: "calculateSubFramePositioning", + subFrameData: { + frameId: 10, + left: expect.any(Number), + parentFrameIds: [1, 2, 3], + top: expect.any(Number), + url: "https://example.com/", + subFrameDepth: expect.any(Number), + }, + }, + "*", + ); + }); + + it("posts the calculated sub frame data to the background", async () => { + document.body.innerHTML = ``; + const iframe = document.querySelector("iframe") as HTMLIFrameElement; + const subFrameData = { + url: "https://example.com/", + frameId: 10, + left: 0, + top: 0, + parentFrameIds: [1, 2, 3], + subFrameDepth: expect.any(Number), + }; + + postWindowMessage( + { command: "calculateSubFramePositioning", subFrameData }, + "*", + iframe.contentWindow as any, + ); + await flushPromises(); + + expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateSubFrameData", { + subFrameData: { + frameId: 10, + left: expect.any(Number), + top: expect.any(Number), + url: "https://example.com/", + parentFrameIds: [1, 2, 3, 4], + subFrameDepth: expect.any(Number), + }, + }); + }); + }); + }); + + describe("checkMostRecentlyFocusedFieldHasValue message handler", () => { + it("returns true if the most recently focused field has a truthy value", async () => { + autofillOverlayContentService["mostRecentlyFocusedField"] = mock< + ElementWithOpId + >({ value: "test" }); + + sendMockExtensionMessage( + { + command: "checkMostRecentlyFocusedFieldHasValue", + }, + mock(), + sendResponseSpy, + ); + await flushPromises(); + + expect(sendResponseSpy).toHaveBeenCalledWith(true); + }); + }); + + describe("setupRebuildSubFrameOffsetsListeners message handler", () => { + let autofillFieldElement: ElementWithOpId; + + beforeEach(() => { + Object.defineProperty(window, "top", { + value: null, + writable: true, + }); + jest.spyOn(globalThis, "addEventListener"); + jest.spyOn(globalThis.document.body, "addEventListener"); + document.body.innerHTML = ` +
+ + +
+ `; + autofillFieldElement = document.getElementById( + "username-field", + ) as ElementWithOpId; + }); + + describe("skipping the setup of the sub frame listeners", () => { + it('skips setup when the window is the "top" frame', async () => { + Object.defineProperty(window, "top", { + value: window, + writable: true, + }); + + sendMockExtensionMessage({ command: "setupRebuildSubFrameOffsetsListeners" }); + await flushPromises(); + + expect(globalThis.addEventListener).not.toHaveBeenCalledWith( + EVENTS.FOCUS, + expect.any(Function), + ); + expect(globalThis.document.body.addEventListener).not.toHaveBeenCalledWith( + EVENTS.MOUSEENTER, + expect.any(Function), + ); + }); + + it("skips setup when no form fields exist on the current frame", async () => { + autofillOverlayContentService["formFieldElements"] = new Set(); + + sendMockExtensionMessage({ command: "setupRebuildSubFrameOffsetsListeners" }); + await flushPromises(); + + expect(globalThis.addEventListener).not.toHaveBeenCalledWith( + EVENTS.FOCUS, + expect.any(Function), + ); + expect(globalThis.document.body.addEventListener).not.toHaveBeenCalledWith( + EVENTS.MOUSEENTER, + expect.any(Function), + ); + }); + }); + + it("sets up the sub frame rebuild listeners when the sub frame contains fields", async () => { + autofillOverlayContentService["formFieldElements"].add(autofillFieldElement); + + sendMockExtensionMessage({ command: "setupRebuildSubFrameOffsetsListeners" }); + await flushPromises(); + + expect(globalThis.addEventListener).toHaveBeenCalledWith( + EVENTS.FOCUS, + expect.any(Function), + ); + expect(globalThis.document.body.addEventListener).toHaveBeenCalledWith( + EVENTS.MOUSEENTER, + expect.any(Function), + ); + }); + + describe("triggering the sub frame listener", () => { + beforeEach(async () => { + autofillOverlayContentService["formFieldElements"].add(autofillFieldElement); + await sendMockExtensionMessage({ command: "setupRebuildSubFrameOffsetsListeners" }); + }); + + it("triggers a rebuild of the sub frame listener when a focus event occurs", async () => { + globalThis.dispatchEvent(new Event(EVENTS.FOCUS)); + await flushPromises(); + + expect(sendExtensionMessageSpy).toHaveBeenCalledWith("triggerSubFrameFocusInRebuild"); + }); + }); + }); + + describe("destroyAutofillInlineMenuListeners message handler", () => { + it("destroys the inline menu listeners", () => { + jest.spyOn(autofillOverlayContentService, "destroy"); + + sendMockExtensionMessage({ command: "destroyAutofillInlineMenuListeners" }); + + expect(autofillOverlayContentService.destroy).toHaveBeenCalled(); + }); }); }); @@ -1670,36 +1679,18 @@ describe("AutofillOverlayContentService", () => { forms: { validFormId: mock() }, fields: [autofillFieldData, passwordFieldData], }); - void autofillOverlayContentService.setupAutofillOverlayListenerOnField( + void autofillOverlayContentService.setupInlineMenu( autofillFieldElement, autofillFieldData, pageDetailsMock, ); autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement; - }); - - it("disconnects all mutation observers", () => { - autofillOverlayContentService["setupMutationObserver"](); - jest.spyOn(autofillOverlayContentService["bodyElementMutationObserver"], "disconnect"); - - autofillOverlayContentService.destroy(); - - expect( - autofillOverlayContentService["bodyElementMutationObserver"].disconnect, - ).toHaveBeenCalled(); - }); - - it("clears the user interaction event timeout", () => { - jest.spyOn(autofillOverlayContentService as any, "clearUserInteractionEventTimeout"); - - autofillOverlayContentService.destroy(); - - expect(autofillOverlayContentService["clearUserInteractionEventTimeout"]).toHaveBeenCalled(); + jest.spyOn(globalThis, "clearTimeout"); + jest.spyOn(globalThis.document, "removeEventListener"); + jest.spyOn(globalThis, "removeEventListener"); }); it("de-registers all global event listeners", () => { - jest.spyOn(globalThis.document, "removeEventListener"); - jest.spyOn(globalThis, "removeEventListener"); jest.spyOn(autofillOverlayContentService as any, "removeOverlayRepositionEventListeners"); autofillOverlayContentService.destroy(); @@ -1739,5 +1730,22 @@ describe("AutofillOverlayContentService", () => { autofillFieldElement, ); }); + + it("clears all existing timeouts", () => { + autofillOverlayContentService["focusInlineMenuListTimeout"] = setTimeout(jest.fn(), 100); + autofillOverlayContentService["closeInlineMenuOnRedirectTimeout"] = setTimeout( + jest.fn(), + 100, + ); + + autofillOverlayContentService.destroy(); + + expect(clearTimeout).toHaveBeenCalledWith( + autofillOverlayContentService["focusInlineMenuListTimeout"], + ); + expect(clearTimeout).toHaveBeenCalledWith( + autofillOverlayContentService["closeInlineMenuOnRedirectTimeout"], + ); + }); }); }); diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index d56a8a80cc6..8148ab98d8a 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -3,71 +3,79 @@ import "lit/polyfill-support.js"; import { FocusableElement, tabbable } from "tabbable"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { EVENTS, AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; +import { + EVENTS, + AutofillOverlayVisibility, + AUTOFILL_OVERLAY_HANDLE_REPOSITION, +} from "@bitwarden/common/autofill/constants"; -import { FocusedFieldData } from "../background/abstractions/overlay.background"; +import { + FocusedFieldData, + SubFrameOffsetData, +} from "../background/abstractions/overlay.background"; +import { AutofillExtensionMessage } from "../content/abstractions/autofill-init"; +import { + AutofillOverlayElement, + MAX_SUB_FRAME_DEPTH, + RedirectFocusDirection, +} from "../enums/autofill-overlay.enum"; import AutofillField from "../models/autofill-field"; import AutofillPageDetails from "../models/autofill-page-details"; -import AutofillOverlayButtonIframe from "../overlay/iframe-content/autofill-overlay-button-iframe"; -import AutofillOverlayListIframe from "../overlay/iframe-content/autofill-overlay-list-iframe"; import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types"; import { elementIsFillableFormField, - generateRandomCustomElementName, + getAttributeBoolean, sendExtensionMessage, - setElementStyles, + throttle, } from "../utils"; -import { AutofillOverlayElement, RedirectFocusDirection } from "../utils/autofill-overlay.enum"; import { + AutofillOverlayContentExtensionMessageHandlers, AutofillOverlayContentService as AutofillOverlayContentServiceInterface, - OpenAutofillOverlayOptions, + OpenAutofillInlineMenuOptions, + SubFrameDataFromWindowMessage, } from "./abstractions/autofill-overlay-content.service"; +import { InlineMenuFieldQualificationService } from "./abstractions/inline-menu-field-qualifications.service"; import { AutoFillConstants } from "./autofill-constants"; -import { InlineMenuFieldQualificationService } from "./inline-menu-field-qualification.service"; -class AutofillOverlayContentService implements AutofillOverlayContentServiceInterface { - private readonly inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; - isFieldCurrentlyFocused = false; - isCurrentlyFilling = false; - isOverlayCiphersPopulated = false; +export class AutofillOverlayContentService implements AutofillOverlayContentServiceInterface { pageDetailsUpdateRequired = false; - autofillOverlayVisibility: number; - private isFirefoxBrowser = - globalThis.navigator.userAgent.indexOf(" Firefox/") !== -1 || - globalThis.navigator.userAgent.indexOf(" Gecko/") !== -1; - private readonly generateRandomCustomElementName = generateRandomCustomElementName; + inlineMenuVisibility: number; private readonly findTabs = tabbable; private readonly sendExtensionMessage = sendExtensionMessage; private formFieldElements: Set> = new Set([]); - private ignoredFieldTypes: Set = new Set(AutoFillConstants.ExcludedOverlayTypes); + private hiddenFormFieldElements: WeakMap, AutofillField> = + new WeakMap(); + private ignoredFieldTypes: Set = new Set(AutoFillConstants.ExcludedInlineMenuTypes); private userFilledFields: Record = {}; private authStatus: AuthenticationStatus; private focusableElements: FocusableElement[] = []; - private isOverlayButtonVisible = false; - private isOverlayListVisible = false; - private overlayButtonElement: HTMLElement; - private overlayListElement: HTMLElement; private mostRecentlyFocusedField: ElementWithOpId; private focusedFieldData: FocusedFieldData; - private userInteractionEventTimeout: number | NodeJS.Timeout; - private overlayElementsMutationObserver: MutationObserver; - private bodyElementMutationObserver: MutationObserver; - private documentElementMutationObserver: MutationObserver; - private mutationObserverIterations = 0; - private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout; - private autofillFieldKeywordsMap: WeakMap = new WeakMap(); + private closeInlineMenuOnRedirectTimeout: number | NodeJS.Timeout; + private focusInlineMenuListTimeout: number | NodeJS.Timeout; private eventHandlersMemo: { [key: string]: EventListener } = {}; - private readonly customElementDefaultStyles: Partial = { - all: "initial", - position: "fixed", - display: "block", - zIndex: "2147483647", + private readonly extensionMessageHandlers: AutofillOverlayContentExtensionMessageHandlers = { + openAutofillInlineMenu: ({ message }) => this.openInlineMenu(message), + addNewVaultItemFromOverlay: () => this.addNewVaultItem(), + blurMostRecentlyFocusedField: () => this.blurMostRecentlyFocusedField(), + unsetMostRecentlyFocusedField: () => this.unsetMostRecentlyFocusedField(), + checkIsMostRecentlyFocusedFieldWithinViewport: () => + this.checkIsMostRecentlyFocusedFieldWithinViewport(), + bgUnlockPopoutOpened: () => this.blurMostRecentlyFocusedField(true), + bgVaultItemRepromptPopoutOpened: () => this.blurMostRecentlyFocusedField(true), + redirectAutofillInlineMenuFocusOut: ({ message }) => + this.redirectInlineMenuFocusOut(message?.data?.direction), + updateAutofillInlineMenuVisibility: ({ message }) => this.updateInlineMenuVisibility(message), + getSubFrameOffsets: ({ message }) => this.getSubFrameOffsets(message), + getSubFrameOffsetsFromWindowMessage: ({ message }) => + this.getSubFrameOffsetsFromWindowMessage(message), + checkMostRecentlyFocusedFieldHasValue: () => this.mostRecentlyFocusedFieldHasValue(), + setupRebuildSubFrameOffsetsListeners: () => this.setupRebuildSubFrameOffsetsListeners(), + destroyAutofillInlineMenuListeners: () => this.destroy(), }; - constructor() { - this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); - } + constructor(private inlineMenuFieldQualificationService: InlineMenuFieldQualificationService) {} /** * Initializes the autofill overlay content service by setting up the mutation observers. @@ -83,14 +91,22 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte } /** - * Sets up the autofill overlay listener on the form field element. This method is called + * Getter used to access the extension message handlers associated + * with the autofill overlay content service. + */ + get messageHandlers(): AutofillOverlayContentExtensionMessageHandlers { + return this.extensionMessageHandlers; + } + + /** + * Sets up the autofill inline menu listener on the form field element. This method is called * during the page details collection process. * * @param formFieldElement - Form field elements identified during the page details collection process. * @param autofillFieldData - Autofill field data captured from the form field element. * @param pageDetails - The collected page details from the tab. */ - async setupAutofillOverlayListenerOnField( + async setupInlineMenu( formFieldElement: ElementWithOpId, autofillFieldData: AutofillField, pageDetails: AutofillPageDetails, @@ -102,49 +118,36 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte return; } - this.formFieldElements.add(formFieldElement); - - if (!this.autofillOverlayVisibility) { - await this.getAutofillOverlayVisibility(); - } - - this.setupFormFieldElementEventListeners(formFieldElement); - - if (this.getRootNodeActiveElement(formFieldElement) === formFieldElement) { - await this.triggerFormFieldFocusedAction(formFieldElement); + if (this.isHiddenField(formFieldElement, autofillFieldData)) { return; } - if (!this.mostRecentlyFocusedField) { - await this.updateMostRecentlyFocusedField(formFieldElement); - } + await this.setupInlineMenuOnQualifiedField(formFieldElement); } /** - * Handles opening the autofill overlay. Will conditionally open - * the overlay based on the current autofill overlay visibility setting. - * Allows you to optionally focus the field element when opening the overlay. - * Will also optionally ignore the overlay visibility setting and open the + * Handles opening the autofill inline menu. Will conditionally open + * the inline menu based on the current inline menu visibility setting. + * Allows you to optionally focus the field element when opening the inline menu. + * Will also optionally ignore the inline menu visibility setting and open the * - * @param options - Options for opening the autofill overlay. + * @param options - Options for opening the autofill inline menu. */ - openAutofillOverlay(options: OpenAutofillOverlayOptions = {}) { - const { isFocusingFieldElement, isOpeningFullOverlay, authStatus } = options; + openInlineMenu(options: OpenAutofillInlineMenuOptions = {}) { + const { isFocusingFieldElement, isOpeningFullInlineMenu, authStatus } = options; if (!this.mostRecentlyFocusedField) { return; } if (this.pageDetailsUpdateRequired) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.sendExtensionMessage("bgCollectPageDetails", { + void this.sendExtensionMessage("bgCollectPageDetails", { sender: "autofillOverlayContentService", }); this.pageDetailsUpdateRequired = false; } if (isFocusingFieldElement && !this.recentlyFocusedFieldIsCurrentlyFocused()) { - this.focusMostRecentOverlayField(); + this.focusMostRecentlyFocusedField(); } if (typeof authStatus !== "undefined") { @@ -152,79 +155,47 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte } if ( - this.autofillOverlayVisibility === AutofillOverlayVisibility.OnButtonClick && - !isOpeningFullOverlay + this.inlineMenuVisibility === AutofillOverlayVisibility.OnButtonClick && + !isOpeningFullInlineMenu ) { - this.updateOverlayButtonPosition(); + this.updateInlineMenuButtonPosition(); return; } - this.updateOverlayElementsPosition(); + this.updateInlineMenuElementsPosition(); } /** * Focuses the most recently focused field element. */ - focusMostRecentOverlayField() { + focusMostRecentlyFocusedField() { this.mostRecentlyFocusedField?.focus(); } /** * Removes focus from the most recently focused field element. */ - blurMostRecentOverlayField() { + blurMostRecentlyFocusedField(isClosingInlineMenu: boolean = false) { this.mostRecentlyFocusedField?.blur(); + + if (isClosingInlineMenu) { + void this.sendExtensionMessage("closeAutofillInlineMenu"); + } } /** - * Removes the autofill overlay from the page. This will initially - * unobserve the body element to ensure the mutation observer no - * longer triggers. + * Sets the most recently focused field within the current frame to a `null` value. */ - removeAutofillOverlay = () => { - this.removeBodyElementObserver(); - this.removeAutofillOverlayButton(); - this.removeAutofillOverlayList(); - }; - - /** - * Removes the overlay button from the DOM if it is currently present. Will - * also remove the overlay reposition event listeners. - */ - removeAutofillOverlayButton() { - if (!this.overlayButtonElement) { - return; - } - - this.overlayButtonElement.remove(); - this.isOverlayButtonVisible = false; - void this.sendExtensionMessage("autofillOverlayElementClosed", { - overlayElement: AutofillOverlayElement.Button, - }); - this.removeOverlayRepositionEventListeners(); - } - - /** - * Removes the overlay list from the DOM if it is currently present. - */ - removeAutofillOverlayList() { - if (!this.overlayListElement) { - return; - } - - this.overlayListElement.remove(); - this.isOverlayListVisible = false; - void this.sendExtensionMessage("autofillOverlayElementClosed", { - overlayElement: AutofillOverlayElement.List, - }); + unsetMostRecentlyFocusedField() { + this.mostRecentlyFocusedField = null; } /** * Formats any found user filled fields for a login cipher and sends a message * to the background script to add a new cipher. */ - addNewVaultItem() { - if (!this.isOverlayListVisible) { + async addNewVaultItem() { + if (!(await this.isInlineMenuListVisible())) { return; } @@ -235,26 +206,27 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte hostname: globalThis.document.location.hostname, }; - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.sendExtensionMessage("autofillOverlayAddNewVaultItem", { login }); + void this.sendExtensionMessage("autofillOverlayAddNewVaultItem", { login }); } /** - * Redirects the keyboard focus out of the overlay, selecting the element that is + * Redirects the keyboard focus out of the inline menu, selecting the element that is * either previous or next in the tab order. If the direction is current, the most * recently focused field will be focused. * - * @param direction - The direction to redirect the focus. + * @param direction - The direction to redirect the focus out. */ - redirectOverlayFocusOut(direction: string) { - if (!this.isOverlayListVisible || !this.mostRecentlyFocusedField) { + private async redirectInlineMenuFocusOut(direction?: string) { + if (!direction || !this.mostRecentlyFocusedField || !(await this.isInlineMenuListVisible())) { return; } if (direction === RedirectFocusDirection.Current) { - this.focusMostRecentOverlayField(); - setTimeout(this.removeAutofillOverlay, 100); + this.focusMostRecentlyFocusedField(); + this.closeInlineMenuOnRedirectTimeout = globalThis.setTimeout( + () => void this.sendExtensionMessage("closeAutofillInlineMenu"), + 100, + ); return; } @@ -274,7 +246,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte /** * Sets up the event listeners that facilitate interaction with the form field elements. * Will clear any cached form field element handlers that are encountered when setting - * up a form field element to the overlay. + * up a form field element. * * @param formFieldElement - The form field element to set up the event listeners for. */ @@ -299,7 +271,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte /** * Removes any cached form field element handlers that are encountered - * when setting up a form field element to present the overlay. + * when setting up a form field element to present the inline menu. * * @param formFieldElement - The form field element to remove the cached handlers for. */ @@ -343,33 +315,35 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte /** * Form Field blur event handler. Updates the value identifying whether - * the field is focused and sends a message to check if the overlay itself + * the field is focused and sends a message to check if the inline menu itself * is currently focused. */ private handleFormFieldBlurEvent = () => { - this.isFieldCurrentlyFocused = false; - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.sendExtensionMessage("checkAutofillOverlayFocused"); + void this.sendExtensionMessage("updateIsFieldCurrentlyFocused", { + isFieldCurrentlyFocused: false, + }); + void this.sendExtensionMessage("checkAutofillInlineMenuFocused"); }; /** * Form field keyup event handler. Facilitates the ability to remove the - * autofill overlay using the escape key, focusing the overlay list using - * the ArrowDown key, and ensuring that the overlay is repositioned when + * autofill inline menu using the escape key, focusing the inline menu list using + * the ArrowDown key, and ensuring that the inline menu is repositioned when * the form is submitted using the Enter key. * * @param event - The keyup event. */ - private handleFormFieldKeyupEvent = (event: KeyboardEvent) => { + private handleFormFieldKeyupEvent = async (event: KeyboardEvent) => { const eventCode = event.code; if (eventCode === "Escape") { - this.removeAutofillOverlay(); + void this.sendExtensionMessage("closeAutofillInlineMenu", { + forceCloseInlineMenu: true, + }); return; } - if (eventCode === "Enter" && !this.isCurrentlyFilling) { - this.handleOverlayRepositionEvent(); + if (eventCode === "Enter" && !(await this.isFieldCurrentlyFilling())) { + void this.handleOverlayRepositionEvent(); return; } @@ -377,28 +351,28 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte event.preventDefault(); event.stopPropagation(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.focusOverlayList(); + void this.focusInlineMenuList(); } }; /** - * Triggers a focus of the overlay list, if it is visible. If the list is not visible, - * the overlay will be opened and the list will be focused after a short delay. Ensures - * that the overlay list is focused when the user presses the down arrow key. + * Triggers a focus of the inline menu list, if it is visible. If the list is not visible, + * the inline menu will be opened and the list will be focused after a short delay. Ensures + * that the inline menu list is focused when the user presses the down arrow key. */ - private async focusOverlayList() { - if (!this.isOverlayListVisible && this.mostRecentlyFocusedField) { + private async focusInlineMenuList() { + if (this.mostRecentlyFocusedField && !(await this.isInlineMenuListVisible())) { + this.clearFocusInlineMenuListTimeout(); await this.updateMostRecentlyFocusedField(this.mostRecentlyFocusedField); - this.openAutofillOverlay({ isOpeningFullOverlay: true }); - setTimeout(() => this.sendExtensionMessage("focusAutofillOverlayList"), 125); + this.openInlineMenu({ isOpeningFullInlineMenu: true }); + this.focusInlineMenuListTimeout = globalThis.setTimeout( + () => this.sendExtensionMessage("focusAutofillInlineMenuList"), + 125, + ); return; } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.sendExtensionMessage("focusAutofillOverlayList"); + void this.sendExtensionMessage("focusAutofillInlineMenuList"); } /** @@ -416,23 +390,26 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte /** * Triggers when the form field element receives an input event. This method will * store the modified form element data for use when the user attempts to add a new - * vault item. It also acts to remove the overlay list while the user is typing. + * vault item. It also acts to remove the inline menu list while the user is typing. * * @param formFieldElement - The form field element that triggered the input event. */ - private triggerFormFieldInput(formFieldElement: ElementWithOpId) { + private async triggerFormFieldInput(formFieldElement: ElementWithOpId) { if (!elementIsFillableFormField(formFieldElement)) { return; } this.storeModifiedFormElement(formFieldElement); - if (formFieldElement.value && (this.isOverlayCiphersPopulated || !this.isUserAuthed())) { - this.removeAutofillOverlayList(); + if (await this.hideInlineMenuListOnFilledField(formFieldElement)) { + void this.sendExtensionMessage("closeAutofillInlineMenu", { + overlayElement: AutofillOverlayElement.List, + forceCloseInlineMenu: true, + }); return; } - this.openAutofillOverlay(); + this.openInlineMenu(); } /** @@ -444,8 +421,8 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte * @private */ private storeModifiedFormElement(formFieldElement: ElementWithOpId) { - if (formFieldElement === this.mostRecentlyFocusedField) { - this.mostRecentlyFocusedField = formFieldElement; + if (formFieldElement !== this.mostRecentlyFocusedField) { + void this.updateMostRecentlyFocusedField(formFieldElement); } if (formFieldElement.type === "password") { @@ -470,12 +447,12 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte /** * Triggers when the form field element receives a click event. This method will - * trigger the focused action for the form field element if the overlay is not visible. + * trigger the focused action for the form field element if the inline menu is not visible. * * @param formFieldElement - The form field element that triggered the click event. */ private async triggerFormFieldClickedAction(formFieldElement: ElementWithOpId) { - if (this.isOverlayButtonVisible || this.isOverlayListVisible) { + if ((await this.isInlineMenuButtonVisible()) || (await this.isInlineMenuListVisible())) { return; } @@ -496,37 +473,39 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte /** * Triggers when the form field element receives a focus event. This method will - * update the most recently focused field and open the autofill overlay if the + * update the most recently focused field and open the autofill inline menu if the * autofill process is not currently active. * * @param formFieldElement - The form field element that triggered the focus event. */ private async triggerFormFieldFocusedAction(formFieldElement: ElementWithOpId) { - if (this.isCurrentlyFilling) { + if (await this.isFieldCurrentlyFilling()) { return; } - this.isFieldCurrentlyFocused = true; - this.clearUserInteractionEventTimeout(); + await this.sendExtensionMessage("updateIsFieldCurrentlyFocused", { + isFieldCurrentlyFocused: true, + }); const initiallyFocusedField = this.mostRecentlyFocusedField; await this.updateMostRecentlyFocusedField(formFieldElement); - const formElementHasValue = Boolean((formFieldElement as HTMLInputElement).value); if ( - this.autofillOverlayVisibility === AutofillOverlayVisibility.OnButtonClick || - (formElementHasValue && initiallyFocusedField !== this.mostRecentlyFocusedField) + this.inlineMenuVisibility === AutofillOverlayVisibility.OnButtonClick || + (initiallyFocusedField !== this.mostRecentlyFocusedField && + (await this.hideInlineMenuListOnFilledField(formFieldElement as FillableFormFieldElement))) ) { - this.removeAutofillOverlayList(); + await this.sendExtensionMessage("closeAutofillInlineMenu", { + overlayElement: AutofillOverlayElement.List, + forceCloseInlineMenu: true, + }); } - if (!formElementHasValue || (!this.isOverlayCiphersPopulated && this.isUserAuthed())) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.sendExtensionMessage("openAutofillOverlay"); + if (await this.hideInlineMenuListOnFilledField(formFieldElement as FillableFormFieldElement)) { + this.updateInlineMenuButtonPosition(); return; } - this.updateOverlayButtonPosition(); + void this.sendExtensionMessage("openAutofillInlineMenu"); } /** @@ -547,82 +526,33 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte } /** - * Updates the position of both the overlay button and overlay list. + * Updates the position of both the inline menu button and list. */ - private updateOverlayElementsPosition() { - this.updateOverlayButtonPosition(); - this.updateOverlayListPosition(); + private updateInlineMenuElementsPosition() { + this.updateInlineMenuButtonPosition(); + this.updateInlineMenuListPosition(); } /** - * Updates the position of the overlay button. + * Updates the position of the inline menu button. */ - private updateOverlayButtonPosition() { - if (!this.overlayButtonElement) { - this.createAutofillOverlayButton(); - this.updateCustomElementDefaultStyles(this.overlayButtonElement); - } - - if (!this.isOverlayButtonVisible) { - this.appendOverlayElementToBody(this.overlayButtonElement); - this.isOverlayButtonVisible = true; - this.setOverlayRepositionEventListeners(); - } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.sendExtensionMessage("updateAutofillOverlayPosition", { + private updateInlineMenuButtonPosition() { + void this.sendExtensionMessage("updateAutofillInlineMenuPosition", { overlayElement: AutofillOverlayElement.Button, }); } /** - * Updates the position of the overlay list. + * Updates the position of the inline menu list. */ - private updateOverlayListPosition() { - if (!this.overlayListElement) { - this.createAutofillOverlayList(); - this.updateCustomElementDefaultStyles(this.overlayListElement); - } - - if (!this.isOverlayListVisible) { - this.appendOverlayElementToBody(this.overlayListElement); - this.isOverlayListVisible = true; - } - - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.sendExtensionMessage("updateAutofillOverlayPosition", { + private updateInlineMenuListPosition() { + void this.sendExtensionMessage("updateAutofillInlineMenuPosition", { overlayElement: AutofillOverlayElement.List, }); } /** - * Appends the overlay element to the body element. This method will also - * observe the body element to ensure that the overlay element is not - * interfered with by any DOM changes. - * - * @param element - The overlay element to append to the body element. - */ - private appendOverlayElementToBody(element: HTMLElement) { - this.observeBodyElement(); - globalThis.document.body.appendChild(element); - } - - /** - * Sends a message that facilitates hiding the overlay elements. - * - * @param isHidden - Indicates if the overlay elements should be hidden. - */ - private toggleOverlayHidden(isHidden: boolean) { - const displayValue = isHidden ? "none" : "block"; - void this.sendExtensionMessage("updateAutofillOverlayHidden", { display: displayValue }); - - this.isOverlayButtonVisible = !!this.overlayButtonElement && !isHidden; - this.isOverlayListVisible = !!this.overlayListElement && !isHidden; - } - - /** - * Updates the data used to position the overlay elements in relation + * Updates the data used to position the inline menu elements in relation * to the most recently focused form field. * * @param formFieldElement - The form field element that triggered the focus event. @@ -630,6 +560,10 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte private async updateMostRecentlyFocusedField( formFieldElement: ElementWithOpId, ) { + if (!formFieldElement || !elementIsFillableFormField(formFieldElement)) { + return; + } + this.mostRecentlyFocusedField = formFieldElement; const { paddingRight, paddingLeft } = globalThis.getComputedStyle(formFieldElement); const { width, height, top, left } = @@ -639,9 +573,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte focusedFieldRects: { width, height, top, left }, }; - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.sendExtensionMessage("updateFocusedFieldData", { + await this.sendExtensionMessage("updateFocusedFieldData", { focusedFieldData: this.focusedFieldData, }); } @@ -701,7 +633,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte } /** - * Identifies if the field should have the autofill overlay setup on it. Currently, this is mainly + * Identifies if the field should have the autofill inline menu setup on it. Currently, this is mainly * determined by whether the field correlates with a login cipher. This method will need to be * updated in the future to support other types of forms. * @@ -712,12 +644,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte autofillFieldData: AutofillField, pageDetails: AutofillPageDetails, ): boolean { - if ( - autofillFieldData.readonly || - autofillFieldData.disabled || - !autofillFieldData.viewable || - this.ignoredFieldTypes.has(autofillFieldData.type) - ) { + if (this.ignoredFieldTypes.has(autofillFieldData.type)) { return true; } @@ -728,354 +655,167 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte } /** - * Creates the autofill overlay button element. Will not attempt - * to create the element if it already exists in the DOM. - */ - private createAutofillOverlayButton() { - if (this.overlayButtonElement) { - return; - } - - if (this.isFirefoxBrowser) { - this.overlayButtonElement = globalThis.document.createElement("div"); - new AutofillOverlayButtonIframe(this.overlayButtonElement); - - return; - } - - const customElementName = this.generateRandomCustomElementName(); - globalThis.customElements?.define( - customElementName, - class extends HTMLElement { - constructor() { - super(); - new AutofillOverlayButtonIframe(this); - } - }, - ); - this.overlayButtonElement = globalThis.document.createElement(customElementName); - } - - /** - * Creates the autofill overlay list element. Will not attempt - * to create the element if it already exists in the DOM. - */ - private createAutofillOverlayList() { - if (this.overlayListElement) { - return; - } - - if (this.isFirefoxBrowser) { - this.overlayListElement = globalThis.document.createElement("div"); - new AutofillOverlayListIframe(this.overlayListElement); - - return; - } - - const customElementName = this.generateRandomCustomElementName(); - globalThis.customElements?.define( - customElementName, - class extends HTMLElement { - constructor() { - super(); - new AutofillOverlayListIframe(this); - } - }, - ); - this.overlayListElement = globalThis.document.createElement(customElementName); - } - - /** - * Updates the default styles for the custom element. This method will - * remove any styles that are added to the custom element by other methods. + * Validates whether a field is considered to be "hidden" based on the field's attributes. + * If the field is hidden, a fallback listener will be set up to ensure that the + * field will have the inline menu set up on it when it becomes visible. * - * @param element - The custom element to update the default styles for. + * @param formFieldElement - The form field element that triggered the focus event. + * @param autofillFieldData - Autofill field data captured from the form field element. */ - private updateCustomElementDefaultStyles(element: HTMLElement) { - this.unobserveCustomElements(); + private isHiddenField( + formFieldElement: ElementWithOpId, + autofillFieldData: AutofillField, + ): boolean { + if (!autofillFieldData.readonly && !autofillFieldData.disabled && autofillFieldData.viewable) { + this.removeHiddenFieldFallbackListener(formFieldElement); + return false; + } - setElementStyles(element, this.customElementDefaultStyles, true); - - this.observeCustomElements(); + this.setupHiddenFieldFallbackListener(formFieldElement, autofillFieldData); + return true; } /** - * Queries the background script for the autofill overlay visibility setting. + * Sets up a fallback listener that will facilitate setting up the + * inline menu on the field when it becomes visible and focused. + * + * @param formFieldElement - The form field element that triggered the focus event. + * @param autofillFieldData - Autofill field data captured from the form field element. + */ + private setupHiddenFieldFallbackListener( + formFieldElement: ElementWithOpId, + autofillFieldData: AutofillField, + ) { + this.hiddenFormFieldElements.set(formFieldElement, autofillFieldData); + formFieldElement.addEventListener(EVENTS.FOCUS, this.handleHiddenFieldFocusEvent); + } + + /** + * Removes the fallback listener that facilitates setting up the inline + * menu on the field when it becomes visible and focused. + * + * @param formFieldElement - The form field element that triggered the focus event. + */ + private removeHiddenFieldFallbackListener(formFieldElement: ElementWithOpId) { + formFieldElement.removeEventListener(EVENTS.FOCUS, this.handleHiddenFieldFocusEvent); + this.hiddenFormFieldElements.delete(formFieldElement); + } + + /** + * Handles the focus event on a hidden field. When + * triggered, the inline menu is set up on the field. + * + * @param event - The focus event. + */ + private handleHiddenFieldFocusEvent = (event: FocusEvent) => { + const formFieldElement = event.target as ElementWithOpId; + const autofillFieldData = this.hiddenFormFieldElements.get(formFieldElement); + if (autofillFieldData) { + autofillFieldData.readonly = getAttributeBoolean(formFieldElement, "disabled"); + autofillFieldData.disabled = getAttributeBoolean(formFieldElement, "disabled"); + autofillFieldData.viewable = true; + void this.setupInlineMenuOnQualifiedField(formFieldElement); + } + + this.removeHiddenFieldFallbackListener(formFieldElement); + }; + + /** + * Sets up the inline menu on a qualified form field element. + * + * @param formFieldElement - The form field element to set up the inline menu on. + */ + private async setupInlineMenuOnQualifiedField( + formFieldElement: ElementWithOpId, + ) { + this.formFieldElements.add(formFieldElement); + + if (!this.mostRecentlyFocusedField) { + await this.updateMostRecentlyFocusedField(formFieldElement); + } + + if (!this.inlineMenuVisibility) { + await this.getInlineMenuVisibility(); + } + + this.setupFormFieldElementEventListeners(formFieldElement); + + if ( + globalThis.document.hasFocus() && + this.getRootNodeActiveElement(formFieldElement) === formFieldElement + ) { + await this.triggerFormFieldFocusedAction(formFieldElement); + } + } + + /** + * Queries the background script for the autofill inline menu visibility setting. * If the setting is not found, a default value of OnFieldFocus will be used * @private */ - private async getAutofillOverlayVisibility() { - const overlayVisibility = await this.sendExtensionMessage("getAutofillOverlayVisibility"); - this.autofillOverlayVisibility = overlayVisibility || AutofillOverlayVisibility.OnFieldFocus; + private async getInlineMenuVisibility() { + const inlineMenuVisibility = await this.sendExtensionMessage("getAutofillInlineMenuVisibility"); + this.inlineMenuVisibility = inlineMenuVisibility || AutofillOverlayVisibility.OnFieldFocus; } /** - * Sets up event listeners that facilitate repositioning - * the autofill overlay on scroll or resize. - */ - private setOverlayRepositionEventListeners() { - globalThis.addEventListener(EVENTS.SCROLL, this.handleOverlayRepositionEvent, { - capture: true, - }); - globalThis.addEventListener(EVENTS.RESIZE, this.handleOverlayRepositionEvent); - } - - /** - * Removes the listeners that facilitate repositioning - * the autofill overlay on scroll or resize. - */ - private removeOverlayRepositionEventListeners() { - globalThis.removeEventListener(EVENTS.SCROLL, this.handleOverlayRepositionEvent, { - capture: true, - }); - globalThis.removeEventListener(EVENTS.RESIZE, this.handleOverlayRepositionEvent); - } - - /** - * Handles the resize or scroll events that enact - * repositioning of the overlay. - */ - private handleOverlayRepositionEvent = () => { - if (!this.isOverlayButtonVisible && !this.isOverlayListVisible) { - return; - } - - this.toggleOverlayHidden(true); - this.clearUserInteractionEventTimeout(); - this.userInteractionEventTimeout = setTimeout( - this.triggerOverlayRepositionUpdates, - 750, - ) as unknown as number; - }; - - /** - * Triggers the overlay reposition updates. This method ensures that the overlay elements - * are correctly positioned when the viewport scrolls or repositions. - */ - private triggerOverlayRepositionUpdates = async () => { - if (!this.recentlyFocusedFieldIsCurrentlyFocused()) { - this.toggleOverlayHidden(false); - this.removeAutofillOverlay(); - return; - } - - await this.updateMostRecentlyFocusedField(this.mostRecentlyFocusedField); - this.updateOverlayElementsPosition(); - this.toggleOverlayHidden(false); - this.clearUserInteractionEventTimeout(); - - if ( - this.focusedFieldData.focusedFieldRects?.top > 0 && - this.focusedFieldData.focusedFieldRects?.top < globalThis.innerHeight - ) { - return; - } - - this.removeAutofillOverlay(); - }; - - /** - * Clears the user interaction event timeout. This is used to ensure that - * the overlay is not repositioned while the user is interacting with it. - */ - private clearUserInteractionEventTimeout() { - if (this.userInteractionEventTimeout) { - clearTimeout(this.userInteractionEventTimeout); - } - } - - /** - * Sets up global event listeners and the mutation - * observer to facilitate required changes to the - * overlay elements. - */ - private setupGlobalEventListeners = () => { - globalThis.document.addEventListener(EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent); - globalThis.addEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent); - this.setupMutationObserver(); - }; - - /** - * Handles the visibility change event. This method will remove the - * autofill overlay if the document is not visible. - */ - private handleVisibilityChangeEvent = () => { - if (document.visibilityState === "visible") { - return; - } - - this.mostRecentlyFocusedField = null; - this.removeAutofillOverlay(); - }; - - /** - * Sets up mutation observers for the overlay elements, the body element, and the - * document element. The mutation observers are used to remove any styles that are - * added to the overlay elements by the website. They are also used to ensure that - * the overlay elements are always present at the bottom of the body element. - */ - private setupMutationObserver = () => { - this.overlayElementsMutationObserver = new MutationObserver( - this.handleOverlayElementMutationObserverUpdate, - ); - - this.bodyElementMutationObserver = new MutationObserver( - this.handleBodyElementMutationObserverUpdate, - ); - }; - - /** - * Sets up mutation observers to verify that the overlay - * elements are not modified by the website. - */ - private observeCustomElements() { - if (this.overlayButtonElement) { - this.overlayElementsMutationObserver?.observe(this.overlayButtonElement, { - attributes: true, - }); - } - - if (this.overlayListElement) { - this.overlayElementsMutationObserver?.observe(this.overlayListElement, { attributes: true }); - } - } - - /** - * Disconnects the mutation observers that are used to verify that the overlay - * elements are not modified by the website. - */ - private unobserveCustomElements() { - this.overlayElementsMutationObserver?.disconnect(); - } - - /** - * Sets up a mutation observer for the body element. The mutation observer is used - * to ensure that the overlay elements are always present at the bottom of the body - * element. - */ - private observeBodyElement() { - this.bodyElementMutationObserver?.observe(globalThis.document.body, { childList: true }); - } - - /** - * Disconnects the mutation observer for the body element. - */ - private removeBodyElementObserver() { - this.bodyElementMutationObserver?.disconnect(); - } - - /** - * Handles the mutation observer update for the overlay elements. This method will - * remove any attributes or styles that might be added to the overlay elements by - * a separate process within the website where this script is injected. + * Returns a value that indicates if we should hide the inline menu list due to a filled field. * - * @param mutationRecord - The mutation record that triggered the update. + * @param formFieldElement - The form field element that triggered the focus event. */ - private handleOverlayElementMutationObserverUpdate = (mutationRecord: MutationRecord[]) => { - if (this.isTriggeringExcessiveMutationObserverIterations()) { - return; - } - - for (let recordIndex = 0; recordIndex < mutationRecord.length; recordIndex++) { - const record = mutationRecord[recordIndex]; - if (record.type !== "attributes") { - continue; - } - - const element = record.target as HTMLElement; - if (record.attributeName !== "style") { - this.removeModifiedElementAttributes(element); - - continue; - } - - element.removeAttribute("style"); - this.updateCustomElementDefaultStyles(element); - } - }; + private async hideInlineMenuListOnFilledField( + formFieldElement?: FillableFormFieldElement, + ): Promise { + return ( + formFieldElement?.value && + ((await this.isInlineMenuCiphersPopulated()) || !this.isUserAuthed()) + ); + } /** - * Removes all elements from a passed overlay - * element except for the style attribute. - * - * @param element - The element to remove the attributes from. + * Indicates whether the most recently focused field has a value. */ - private removeModifiedElementAttributes(element: HTMLElement) { - const attributes = Array.from(element.attributes); - for (let attributeIndex = 0; attributeIndex < attributes.length; attributeIndex++) { - const attribute = attributes[attributeIndex]; - if (attribute.name === "style") { - continue; - } + private mostRecentlyFocusedFieldHasValue() { + return Boolean((this.mostRecentlyFocusedField as FillableFormFieldElement)?.value); + } - element.removeAttribute(attribute.name); + /** + * Updates the local reference to the inline menu visibility setting. + * + * @param data - The data object from the extension message. + */ + private updateInlineMenuVisibility({ data }: AutofillExtensionMessage) { + if (!isNaN(data?.inlineMenuVisibility)) { + this.inlineMenuVisibility = data.inlineMenuVisibility; } } /** - * Handles the mutation observer update for the body element. This method will - * ensure that the overlay elements are always present at the bottom of the body - * element. + * Checks if a field is currently filling within an frame in the tab. */ - private handleBodyElementMutationObserverUpdate = () => { - if ( - (!this.overlayButtonElement && !this.overlayListElement) || - this.isTriggeringExcessiveMutationObserverIterations() - ) { - return; - } - - const lastChild = globalThis.document.body.lastElementChild; - const secondToLastChild = lastChild?.previousElementSibling; - const lastChildIsOverlayList = lastChild === this.overlayListElement; - const lastChildIsOverlayButton = lastChild === this.overlayButtonElement; - const secondToLastChildIsOverlayButton = secondToLastChild === this.overlayButtonElement; - - if ( - (lastChildIsOverlayList && secondToLastChildIsOverlayButton) || - (lastChildIsOverlayButton && !this.isOverlayListVisible) - ) { - return; - } - - if ( - (lastChildIsOverlayList && !secondToLastChildIsOverlayButton) || - (lastChildIsOverlayButton && this.isOverlayListVisible) - ) { - globalThis.document.body.insertBefore(this.overlayButtonElement, this.overlayListElement); - return; - } - - globalThis.document.body.insertBefore(lastChild, this.overlayButtonElement); - }; + private async isFieldCurrentlyFilling() { + return (await this.sendExtensionMessage("checkIsFieldCurrentlyFilling")) === true; + } /** - * Identifies if the mutation observer is triggering excessive iterations. - * Will trigger a blur of the most recently focused field and remove the - * autofill overlay if any set mutation observer is triggering - * excessive iterations. + * Checks if the inline menu button is visible at the top frame. */ - private isTriggeringExcessiveMutationObserverIterations() { - if (this.mutationObserverIterationsResetTimeout) { - clearTimeout(this.mutationObserverIterationsResetTimeout); - } + private async isInlineMenuButtonVisible() { + return (await this.sendExtensionMessage("checkIsAutofillInlineMenuButtonVisible")) === true; + } - this.mutationObserverIterations++; - this.mutationObserverIterationsResetTimeout = setTimeout( - () => (this.mutationObserverIterations = 0), - 2000, - ); + /** + * Checks if the inline menu list if visible at the top frame. + */ + private async isInlineMenuListVisible() { + return (await this.sendExtensionMessage("checkIsAutofillInlineMenuListVisible")) === true; + } - if (this.mutationObserverIterations > 100) { - clearTimeout(this.mutationObserverIterationsResetTimeout); - this.mutationObserverIterations = 0; - this.blurMostRecentOverlayField(); - this.removeAutofillOverlay(); - - return true; - } - - return false; + /** + * Checks if the current tab contains ciphers that can be used to populate the inline menu. + */ + private async isInlineMenuCiphersPopulated() { + return (await this.sendExtensionMessage("checkIsInlineMenuCiphersPopulated")) === true; } /** @@ -1084,31 +824,394 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte * @param element - The element to get the root node active element for. */ private getRootNodeActiveElement(element: Element): Element { + if (!element) { + return null; + } + const documentRoot = element.getRootNode() as ShadowRoot | Document; return documentRoot?.activeElement; } + /** + * Queries all iframe elements within the document and returns the + * sub frame offsets for each iframe element. + * + * @param message - The message object from the extension. + */ + private async getSubFrameOffsets( + message: AutofillExtensionMessage, + ): Promise { + const { subFrameUrl } = message; + + const subFrameUrlVariations = this.getSubFrameUrlVariations(subFrameUrl); + if (!subFrameUrlVariations) { + return null; + } + + let iframeElement: HTMLIFrameElement | null = null; + const iframeElements = globalThis.document.getElementsByTagName("iframe"); + + for (let iframeIndex = 0; iframeIndex < iframeElements.length; iframeIndex++) { + const iframe = iframeElements[iframeIndex]; + if (!subFrameUrlVariations.has(iframe.src)) { + continue; + } + + if (iframeElement) { + return null; + } + + iframeElement = iframe; + } + + if (!iframeElement) { + return null; + } + + return this.calculateSubFrameOffsets(iframeElement, subFrameUrl); + } + + /** + * Returns a set of all possible URL variations for the sub frame URL. + * + * @param subFrameUrl - The URL of the sub frame. + */ + private getSubFrameUrlVariations(subFrameUrl: string) { + try { + const url = new URL(subFrameUrl, globalThis.location.href); + const pathAndHash = url.pathname + url.hash; + const pathAndSearch = url.pathname + url.search; + const pathSearchAndHash = pathAndSearch + url.hash; + const pathNameWithoutTrailingSlash = url.pathname.replace(/\/$/, ""); + const pathWithoutTrailingSlashAndHash = pathNameWithoutTrailingSlash + url.hash; + const pathWithoutTrailingSlashAndSearch = pathNameWithoutTrailingSlash + url.search; + const pathWithoutTrailingSlashSearchAndHash = pathWithoutTrailingSlashAndSearch + url.hash; + + return new Set([ + url.href, + url.href.replace(/\/$/, ""), + url.pathname, + pathAndHash, + pathAndSearch, + pathSearchAndHash, + pathNameWithoutTrailingSlash, + pathWithoutTrailingSlashAndHash, + pathWithoutTrailingSlashAndSearch, + pathWithoutTrailingSlashSearchAndHash, + url.hostname + url.pathname, + url.hostname + pathAndHash, + url.hostname + pathAndSearch, + url.hostname + pathSearchAndHash, + url.hostname + pathNameWithoutTrailingSlash, + url.hostname + pathWithoutTrailingSlashAndHash, + url.hostname + pathWithoutTrailingSlashAndSearch, + url.hostname + pathWithoutTrailingSlashSearchAndHash, + url.origin + url.pathname, + url.origin + pathAndHash, + url.origin + pathAndSearch, + url.origin + pathSearchAndHash, + url.origin + pathNameWithoutTrailingSlash, + url.origin + pathWithoutTrailingSlashAndHash, + url.origin + pathWithoutTrailingSlashAndSearch, + url.origin + pathWithoutTrailingSlashSearchAndHash, + ]); + } catch (_error) { + return null; + } + } + + /** + * Posts a message to the parent frame to calculate the sub frame offset of the current frame. + * + * @param message - The message object from the extension. + */ + private getSubFrameOffsetsFromWindowMessage(message: any) { + globalThis.parent.postMessage( + { + command: "calculateSubFramePositioning", + subFrameData: { + url: window.location.href, + frameId: message.subFrameId, + left: 0, + top: 0, + parentFrameIds: [0], + subFrameDepth: 0, + } as SubFrameDataFromWindowMessage, + }, + "*", + ); + } + + /** + * Calculates the bounding rect for the queried frame and returns the + * offset data for the sub frame. + * + * @param iframeElement - The iframe element to calculate the sub frame offsets for. + * @param subFrameUrl - The URL of the sub frame. + * @param frameId - The frame ID of the sub frame. + */ + private calculateSubFrameOffsets( + iframeElement: HTMLIFrameElement, + subFrameUrl?: string, + frameId?: number, + ): SubFrameOffsetData { + const iframeRect = iframeElement.getBoundingClientRect(); + const iframeStyles = globalThis.getComputedStyle(iframeElement); + const paddingLeft = parseInt(iframeStyles.getPropertyValue("padding-left")) || 0; + const paddingTop = parseInt(iframeStyles.getPropertyValue("padding-top")) || 0; + const borderWidthLeft = parseInt(iframeStyles.getPropertyValue("border-left-width")) || 0; + const borderWidthTop = parseInt(iframeStyles.getPropertyValue("border-top-width")) || 0; + + return { + url: subFrameUrl, + frameId, + top: iframeRect.top + paddingTop + borderWidthTop, + left: iframeRect.left + paddingLeft + borderWidthLeft, + }; + } + + /** + * Calculates the sub frame positioning for the current frame + * through all parent frames until the top frame is reached. + * + * @param event - The message event. + */ + private calculateSubFramePositioning = async (event: MessageEvent) => { + const subFrameData: SubFrameDataFromWindowMessage = event.data.subFrameData; + + subFrameData.subFrameDepth++; + if (subFrameData.subFrameDepth >= MAX_SUB_FRAME_DEPTH) { + void this.sendExtensionMessage("destroyAutofillInlineMenuListeners", { subFrameData }); + return; + } + + let subFrameOffsets: SubFrameOffsetData; + const iframes = globalThis.document.querySelectorAll("iframe"); + for (let i = 0; i < iframes.length; i++) { + if (iframes[i].contentWindow === event.source) { + const iframeElement = iframes[i]; + subFrameOffsets = this.calculateSubFrameOffsets( + iframeElement, + subFrameData.url, + subFrameData.frameId, + ); + + subFrameData.top += subFrameOffsets.top; + subFrameData.left += subFrameOffsets.left; + + const parentFrameId = await this.sendExtensionMessage("getCurrentTabFrameId"); + if (typeof parentFrameId !== "undefined") { + subFrameData.parentFrameIds.push(parentFrameId); + } + + break; + } + } + + if (globalThis.window.self !== globalThis.window.top) { + globalThis.parent.postMessage({ command: "calculateSubFramePositioning", subFrameData }, "*"); + return; + } + + void this.sendExtensionMessage("updateSubFrameData", { subFrameData }); + }; + + /** + * Sets up global event listeners and the mutation + * observer to facilitate required changes to the + * overlay elements. + */ + private setupGlobalEventListeners = () => { + globalThis.addEventListener(EVENTS.MESSAGE, this.handleWindowMessageEvent); + globalThis.document.addEventListener(EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent); + globalThis.addEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent); + this.setOverlayRepositionEventListeners(); + }; + + /** + * Handles window messages that are sent to the current frame. Will trigger a + * calculation of the sub frame offsets through the parent frame. + * + * @param event - The message event. + */ + private handleWindowMessageEvent = (event: MessageEvent) => { + if (event.data?.command === "calculateSubFramePositioning") { + void this.calculateSubFramePositioning(event); + } + }; + + /** + * Handles the visibility change event. This method will remove the + * autofill overlay if the document is not visible. + */ + private handleVisibilityChangeEvent = () => { + if (!this.mostRecentlyFocusedField || globalThis.document.visibilityState === "visible") { + return; + } + + this.unsetMostRecentlyFocusedField(); + void this.sendExtensionMessage("closeAutofillInlineMenu", { + forceCloseInlineMenu: true, + }); + }; + + /** + * Sets up event listeners that facilitate repositioning + * the overlay elements on scroll or resize. + */ + private setOverlayRepositionEventListeners() { + const handler = this.useEventHandlersMemo( + throttle(this.handleOverlayRepositionEvent, 250), + AUTOFILL_OVERLAY_HANDLE_REPOSITION, + ); + globalThis.addEventListener(EVENTS.SCROLL, handler, { + capture: true, + passive: true, + }); + globalThis.addEventListener(EVENTS.RESIZE, handler); + } + + /** + * Removes the listeners that facilitate repositioning + * the overlay elements on scroll or resize. + */ + private removeOverlayRepositionEventListeners() { + const handler = this.eventHandlersMemo[AUTOFILL_OVERLAY_HANDLE_REPOSITION]; + globalThis.removeEventListener(EVENTS.SCROLL, handler, { + capture: true, + }); + globalThis.removeEventListener(EVENTS.RESIZE, handler); + + delete this.eventHandlersMemo[AUTOFILL_OVERLAY_HANDLE_REPOSITION]; + } + + /** + * Handles the resize or scroll events that enact + * repositioning of existing overlay elements. + */ + private handleOverlayRepositionEvent = async () => { + await this.sendExtensionMessage("triggerAutofillOverlayReposition"); + }; + + /** + * Sets up listeners that facilitate a rebuild of the sub frame offsets + * when a user interacts or focuses an element within the frame. + */ + private setupRebuildSubFrameOffsetsListeners = () => { + if (globalThis.window.top === globalThis.window || this.formFieldElements.size < 1) { + return; + } + this.removeSubFrameFocusOutListeners(); + + globalThis.addEventListener(EVENTS.FOCUS, this.handleSubFrameFocusInEvent); + globalThis.document.body.addEventListener(EVENTS.MOUSEENTER, this.handleSubFrameFocusInEvent); + }; + + /** + * Removes the listeners that facilitate a rebuild of the sub frame offsets. + */ + private removeRebuildSubFrameOffsetsListeners = () => { + globalThis.removeEventListener(EVENTS.FOCUS, this.handleSubFrameFocusInEvent); + globalThis.document.body.removeEventListener( + EVENTS.MOUSEENTER, + this.handleSubFrameFocusInEvent, + ); + }; + + /** + * Re-establishes listeners that handle the sub frame offsets rebuild of the frame + * based on user interaction with the sub frame. + */ + private setupSubFrameFocusOutListeners = () => { + globalThis.addEventListener(EVENTS.BLUR, this.setupRebuildSubFrameOffsetsListeners); + globalThis.document.body.addEventListener( + EVENTS.MOUSELEAVE, + this.setupRebuildSubFrameOffsetsListeners, + ); + }; + + /** + * Removes the listeners that trigger when a user focuses away from the sub frame. + */ + private removeSubFrameFocusOutListeners = () => { + globalThis.removeEventListener(EVENTS.BLUR, this.setupRebuildSubFrameOffsetsListeners); + globalThis.document.body.removeEventListener( + EVENTS.MOUSELEAVE, + this.setupRebuildSubFrameOffsetsListeners, + ); + }; + + /** + * Sends a message to the background script to trigger a rebuild of the sub frame + * offsets. Will deregister the listeners to ensure that other focus and mouse + * events do not unnecessarily re-trigger a sub frame rebuild. + */ + private handleSubFrameFocusInEvent = () => { + void this.sendExtensionMessage("triggerSubFrameFocusInRebuild"); + + this.removeRebuildSubFrameOffsetsListeners(); + this.setupSubFrameFocusOutListeners(); + }; + + /** + * Triggers an update in the most recently focused field's data and returns + * whether the field is within the viewport bounds. If not within the bounds + * of the viewport, the inline menu will be closed. + */ + private async checkIsMostRecentlyFocusedFieldWithinViewport() { + await this.updateMostRecentlyFocusedField(this.mostRecentlyFocusedField); + + const focusedFieldRectsTop = this.focusedFieldData?.focusedFieldRects?.top; + const focusedFieldRectsBottom = + focusedFieldRectsTop + this.focusedFieldData?.focusedFieldRects?.height; + const viewportHeight = globalThis.innerHeight + globalThis.scrollY; + return ( + focusedFieldRectsTop && + focusedFieldRectsTop > 0 && + focusedFieldRectsTop < viewportHeight && + focusedFieldRectsBottom < viewportHeight + ); + } + + /** + * Clears the timeout that triggers a debounced focus of the inline menu list. + */ + private clearFocusInlineMenuListTimeout() { + if (this.focusInlineMenuListTimeout) { + globalThis.clearTimeout(this.focusInlineMenuListTimeout); + } + } + + /** + * Clears the timeout that triggers the closing of the inline menu on a focus redirection. + */ + private clearCloseInlineMenuOnRedirectTimeout() { + if (this.closeInlineMenuOnRedirectTimeout) { + globalThis.clearTimeout(this.closeInlineMenuOnRedirectTimeout); + } + } + /** * Destroys the autofill overlay content service. This method will * disconnect the mutation observers and remove all event listeners. */ destroy() { - this.documentElementMutationObserver?.disconnect(); - this.clearUserInteractionEventTimeout(); + this.clearFocusInlineMenuListTimeout(); + this.clearCloseInlineMenuOnRedirectTimeout(); this.formFieldElements.forEach((formFieldElement) => { this.removeCachedFormFieldEventListeners(formFieldElement); formFieldElement.removeEventListener(EVENTS.BLUR, this.handleFormFieldBlurEvent); formFieldElement.removeEventListener(EVENTS.KEYUP, this.handleFormFieldKeyupEvent); this.formFieldElements.delete(formFieldElement); }); + globalThis.removeEventListener(EVENTS.MESSAGE, this.handleWindowMessageEvent); globalThis.document.removeEventListener( EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent, ); globalThis.removeEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent); - this.removeAutofillOverlay(); this.removeOverlayRepositionEventListeners(); + this.removeRebuildSubFrameOffsetsListeners(); + this.removeSubFrameFocusOutListeners(); } } - -export default AutofillOverlayContentService; diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index dc9f3fcdbd4..ce7f4d41d26 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -5,7 +5,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; -import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DefaultDomainSettingsService, DomainSettingsService, @@ -14,6 +14,7 @@ import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { MessageListener } from "@bitwarden/common/platform/messaging"; @@ -40,7 +41,7 @@ import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { BrowserApi } from "../../platform/browser/browser-api"; import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; import { AutofillMessageCommand, AutofillMessageSender } from "../enums/autofill-message.enums"; -import { AutofillPort } from "../enums/autofill-port.enums"; +import { AutofillPort } from "../enums/autofill-port.enum"; import AutofillField from "../models/autofill-field"; import AutofillPageDetails from "../models/autofill-page-details"; import AutofillScript from "../models/autofill-script"; @@ -72,7 +73,7 @@ describe("AutofillService", () => { let autofillService: AutofillService; const cipherService = mock(); let inlineMenuVisibilityMock$!: BehaviorSubject; - let autofillSettingsService: MockProxy; + let autofillSettingsService: MockProxy; const mockUserId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService); @@ -86,16 +87,18 @@ describe("AutofillService", () => { const platformUtilsService = mock(); let activeAccountStatusMock$: BehaviorSubject; let authService: MockProxy; + let configService: MockProxy; let messageListener: MockProxy; beforeEach(() => { scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService); inlineMenuVisibilityMock$ = new BehaviorSubject(AutofillOverlayVisibility.OnFieldFocus); - autofillSettingsService = mock(); - (autofillSettingsService as any).inlineMenuVisibility$ = inlineMenuVisibilityMock$; + autofillSettingsService = mock(); + autofillSettingsService.inlineMenuVisibility$ = inlineMenuVisibilityMock$; activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked); authService = mock(); authService.activeAccountStatus$ = activeAccountStatusMock$; + configService = mock(); messageListener = mock(); autofillService = new AutofillService( cipherService, @@ -109,6 +112,7 @@ describe("AutofillService", () => { scriptInjectorService, accountService, authService, + configService, messageListener, ); domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); @@ -213,7 +217,7 @@ describe("AutofillService", () => { .spyOn(BrowserApi, "getAllFrameDetails") .mockResolvedValue([mock({ frameId: 0 })]); jest - .spyOn(autofillService, "getOverlayVisibility") + .spyOn(autofillService, "getInlineMenuVisibility") .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true); }); @@ -275,13 +279,13 @@ describe("AutofillService", () => { expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( tab1, - "updateAutofillOverlayVisibility", - { autofillOverlayVisibility: AutofillOverlayVisibility.OnButtonClick }, + "updateAutofillInlineMenuVisibility", + { inlineMenuVisibility: AutofillOverlayVisibility.OnButtonClick }, ); expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( tab2, - "updateAutofillOverlayVisibility", - { autofillOverlayVisibility: AutofillOverlayVisibility.OnButtonClick }, + "updateAutofillInlineMenuVisibility", + { inlineMenuVisibility: AutofillOverlayVisibility.OnButtonClick }, ); }); @@ -292,13 +296,13 @@ describe("AutofillService", () => { expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( tab1, - "updateAutofillOverlayVisibility", - { autofillOverlayVisibility: AutofillOverlayVisibility.OnFieldFocus }, + "updateAutofillInlineMenuVisibility", + { inlineMenuVisibility: AutofillOverlayVisibility.OnFieldFocus }, ); expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( tab2, - "updateAutofillOverlayVisibility", - { autofillOverlayVisibility: AutofillOverlayVisibility.OnFieldFocus }, + "updateAutofillInlineMenuVisibility", + { inlineMenuVisibility: AutofillOverlayVisibility.OnFieldFocus }, ); }); }); @@ -351,11 +355,12 @@ describe("AutofillService", () => { let sender: chrome.runtime.MessageSender; beforeEach(() => { + configService.getFeatureFlag.mockResolvedValue(true); tabMock = createChromeTabMock(); sender = { tab: tabMock, frameId: 1 }; jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation(); jest - .spyOn(autofillService, "getOverlayVisibility") + .spyOn(autofillService, "getInlineMenuVisibility") .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true); }); @@ -413,7 +418,7 @@ describe("AutofillService", () => { it("will inject the bootstrap-autofill script if the user does not have the autofill overlay enabled", async () => { jest - .spyOn(autofillService, "getOverlayVisibility") + .spyOn(autofillService, "getInlineMenuVisibility") .mockResolvedValue(AutofillOverlayVisibility.Off); await autofillService.injectAutofillScripts(sender.tab, sender.frameId); diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 4c37cd1f07f..81a47b2f614 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -12,10 +12,12 @@ import { DomainSettingsService } from "@bitwarden/common/autofill/services/domai import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { UriMatchStrategySetting, UriMatchStrategy, } from "@bitwarden/common/models/domain/domain-service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessageListener } from "@bitwarden/common/platform/messaging"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -29,7 +31,7 @@ import { BrowserApi } from "../../platform/browser/browser-api"; import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; import { openVaultItemPasswordRepromptPopout } from "../../vault/popup/utils/vault-popout-window"; import { AutofillMessageCommand, AutofillMessageSender } from "../enums/autofill-message.enums"; -import { AutofillPort } from "../enums/autofill-port.enums"; +import { AutofillPort } from "../enums/autofill-port.enum"; import AutofillField from "../models/autofill-field"; import AutofillPageDetails from "../models/autofill-page-details"; import AutofillScript from "../models/autofill-script"; @@ -67,6 +69,7 @@ export default class AutofillService implements AutofillServiceInterface { private scriptInjectorService: ScriptInjectorService, private accountService: AccountService, private authService: AuthService, + private configService: ConfigService, private messageListener: MessageListener, ) {} @@ -160,16 +163,23 @@ export default class AutofillService implements AutofillServiceInterface { const activeAccount = await firstValueFrom(this.accountService.activeAccount$); const authStatus = await firstValueFrom(this.authService.activeAccountStatus$); const accountIsUnlocked = authStatus === AuthenticationStatus.Unlocked; - let overlayVisibility: InlineMenuVisibilitySetting = AutofillOverlayVisibility.Off; + let inlineMenuVisibility: InlineMenuVisibilitySetting = AutofillOverlayVisibility.Off; let autoFillOnPageLoadIsEnabled = false; if (activeAccount) { - overlayVisibility = await this.getOverlayVisibility(); + inlineMenuVisibility = await this.getInlineMenuVisibility(); } - const mainAutofillScript = overlayVisibility - ? "bootstrap-autofill-overlay.js" - : "bootstrap-autofill.js"; + let mainAutofillScript = "bootstrap-autofill.js"; + + if (inlineMenuVisibility) { + const inlineMenuPositioningImprovements = await this.configService.getFeatureFlag( + FeatureFlag.InlineMenuPositioningImprovements, + ); + mainAutofillScript = inlineMenuPositioningImprovements + ? "bootstrap-autofill-overlay.js" + : "bootstrap-legacy-autofill-overlay.js"; + } const injectedScripts = [mainAutofillScript]; @@ -274,7 +284,7 @@ export default class AutofillService implements AutofillServiceInterface { /** * Gets the overlay's visibility setting from the autofill settings service. */ - async getOverlayVisibility(): Promise { + async getInlineMenuVisibility(): Promise { return await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$); } @@ -2162,8 +2172,8 @@ export default class AutofillService implements AutofillServiceInterface { if (!inlineMenuPreviouslyDisabled && !inlineMenuCurrentlyDisabled) { const tabs = await BrowserApi.tabsQuery({}); tabs.forEach((tab) => - BrowserApi.tabSendMessageData(tab, "updateAutofillOverlayVisibility", { - autofillOverlayVisibility: currentSetting, + BrowserApi.tabSendMessageData(tab, "updateAutofillInlineMenuVisibility", { + inlineMenuVisibility: currentSetting, }), ); return; diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts index 9bb0e717a26..f67c0e88aa0 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts @@ -11,7 +11,8 @@ import { FormElementWithAttribute, } from "../types"; -import AutofillOverlayContentService from "./autofill-overlay-content.service"; +import { InlineMenuFieldQualificationService } from "./abstractions/inline-menu-field-qualifications.service"; +import { AutofillOverlayContentService } from "./autofill-overlay-content.service"; import CollectAutofillContentService from "./collect-autofill-content.service"; import DomElementVisibilityService from "./dom-element-visibility.service"; @@ -28,7 +29,10 @@ const waitForIdleCallback = () => new Promise((resolve) => globalThis.requestIdl describe("CollectAutofillContentService", () => { const domElementVisibilityService = new DomElementVisibilityService(); - const autofillOverlayContentService = new AutofillOverlayContentService(); + const inlineMenuFieldQualificationService = mock(); + const autofillOverlayContentService = new AutofillOverlayContentService( + inlineMenuFieldQualificationService, + ); let collectAutofillContentService: CollectAutofillContentService; const mockIntersectionObserver = mock(); const mockQuerySelectorAll = mockQuerySelectorAllDefinedCall(); @@ -250,7 +254,7 @@ describe("CollectAutofillContentService", () => { .mockResolvedValue(true); const setupAutofillOverlayListenerOnFieldSpy = jest.spyOn( collectAutofillContentService["autofillOverlayContentService"], - "setupAutofillOverlayListenerOnField", + "setupInlineMenu", ); await collectAutofillContentService.getPageDetails(); @@ -2564,7 +2568,7 @@ describe("CollectAutofillContentService", () => { ); setupAutofillOverlayListenerOnFieldSpy = jest.spyOn( collectAutofillContentService["autofillOverlayContentService"], - "setupAutofillOverlayListenerOnField", + "setupInlineMenu", ); }); @@ -2585,9 +2589,11 @@ describe("CollectAutofillContentService", () => { it("skips setting up the overlay listeners on a field that is not viewable", async () => { const formFieldElement = document.createElement("input") as ElementWithOpId; + const autofillField = mock(); const entries = [ { target: formFieldElement, isIntersecting: true }, ] as unknown as IntersectionObserverEntry[]; + collectAutofillContentService["autofillFieldElements"].set(formFieldElement, autofillField); isFormFieldViewableSpy.mockReturnValueOnce(false); await collectAutofillContentService["handleFormElementIntersection"](entries); @@ -2596,7 +2602,21 @@ describe("CollectAutofillContentService", () => { expect(setupAutofillOverlayListenerOnFieldSpy).not.toHaveBeenCalled(); }); - it("sets up the overlay listeners on a viewable field", async () => { + it("skips setting up the inline menu listeners if the observed form field is not present in the cache", async () => { + const formFieldElement = document.createElement("input") as ElementWithOpId; + const entries = [ + { target: formFieldElement, isIntersecting: true }, + ] as unknown as IntersectionObserverEntry[]; + isFormFieldViewableSpy.mockReturnValueOnce(true); + collectAutofillContentService["intersectionObserver"] = mockIntersectionObserver; + + await collectAutofillContentService["handleFormElementIntersection"](entries); + + expect(isFormFieldViewableSpy).not.toHaveBeenCalled(); + expect(setupAutofillOverlayListenerOnFieldSpy).not.toHaveBeenCalled(); + }); + + it("sets up the inline menu listeners on a viewable field", async () => { const formFieldElement = document.createElement("input") as ElementWithOpId; const autofillField = mock(); const entries = [ @@ -2616,4 +2636,17 @@ describe("CollectAutofillContentService", () => { ); }); }); + + describe("destroy", () => { + it("clears the updateAfterMutationIdleCallback", () => { + jest.spyOn(window, "clearTimeout"); + collectAutofillContentService["updateAfterMutationIdleCallback"] = setTimeout(jest.fn, 100); + + collectAutofillContentService.destroy(); + + expect(clearTimeout).toHaveBeenCalledWith( + collectAutofillContentService["updateAfterMutationIdleCallback"], + ); + }); + }); }); diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.ts index 75c564e868e..b5541ba5eb6 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -1,12 +1,7 @@ import AutofillField from "../models/autofill-field"; import AutofillForm from "../models/autofill-form"; import AutofillPageDetails from "../models/autofill-page-details"; -import { - ElementWithOpId, - FillableFormFieldElement, - FormElementWithAttribute, - FormFieldElement, -} from "../types"; +import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types"; import { elementIsDescriptionDetailsElement, elementIsDescriptionTermElement, @@ -21,6 +16,8 @@ import { nodeIsFormElement, nodeIsInputElement, // sendExtensionMessage, + getAttributeBoolean, + getPropertyOrAttribute, requestIdleCallbackPolyfill, cancelIdleCallbackPolyfill, } from "../utils"; @@ -37,6 +34,8 @@ import { DomElementVisibilityService } from "./abstractions/dom-element-visibili class CollectAutofillContentService implements CollectAutofillContentServiceInterface { private readonly domElementVisibilityService: DomElementVisibilityService; private readonly autofillOverlayContentService: AutofillOverlayContentService; + private readonly getAttributeBoolean = getAttributeBoolean; + private readonly getPropertyOrAttribute = getPropertyOrAttribute; private noFieldsFound = false; private domRecentlyMutated = true; private autofillFormElements: AutofillFormElements = new Map(); @@ -286,7 +285,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte autofillField.viewable = await this.domElementVisibilityService.isFormFieldViewable(element); if (!previouslyViewable && autofillField.viewable) { - this.setupInlineMenuListenerOnField(element, autofillField); + this.setupInlineMenu(element, autofillField); } }); } @@ -537,26 +536,6 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte ); } - /** - * Returns a boolean representing the attribute value of an element. - * @param {ElementWithOpId} element - * @param {string} attributeName - * @param {boolean} checkString - * @returns {boolean} - * @private - */ - private getAttributeBoolean( - element: ElementWithOpId, - attributeName: string, - checkString = false, - ): boolean { - if (checkString) { - return this.getPropertyOrAttribute(element, attributeName) === "true"; - } - - return Boolean(this.getPropertyOrAttribute(element, attributeName)); - } - /** * Returns the attribute of an element as a lowercase value. * @param {ElementWithOpId} element @@ -868,21 +847,6 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte return this.recursivelyGetTextFromPreviousSiblings(siblingElement); } - /** - * Get the value of a property or attribute from a FormFieldElement. - * @param {HTMLElement} element - * @param {string} attributeName - * @returns {string | null} - * @private - */ - private getPropertyOrAttribute(element: HTMLElement, attributeName: string): string | null { - if (attributeName in element) { - return (element as FormElementWithAttribute)[attributeName]; - } - - return element.getAttribute(attributeName); - } - /** * Gets the value of the element. If the element is a checkbox, returns a checkmark if the * checkbox is checked, or an empty string if it is not checked. If the element is a hidden @@ -1411,20 +1375,20 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte continue; } + const cachedAutofillFieldElement = this.autofillFieldElements.get(formFieldElement); + if (!cachedAutofillFieldElement) { + this.intersectionObserver.unobserve(entry.target); + continue; + } + const isViewable = await this.domElementVisibilityService.isFormFieldViewable(formFieldElement); if (!isViewable) { continue; } - const cachedAutofillFieldElement = this.autofillFieldElements.get(formFieldElement); - if (!cachedAutofillFieldElement) { - continue; - } - cachedAutofillFieldElement.viewable = true; - - this.setupInlineMenuListenerOnField(formFieldElement, cachedAutofillFieldElement); + this.setupInlineMenu(formFieldElement, cachedAutofillFieldElement); this.intersectionObserver?.unobserve(entry.target); } @@ -1441,7 +1405,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte } this.autofillFieldElements.forEach((autofillField, formFieldElement) => { - this.setupInlineMenuListenerOnField(formFieldElement, autofillField, pageDetails); + this.setupInlineMenu(formFieldElement, autofillField, pageDetails); }); } @@ -1452,7 +1416,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte * @param autofillField - The metadata for the form field * @param pageDetails - The page details to use for the inline menu listeners */ - private setupInlineMenuListenerOnField( + private setupInlineMenu( formFieldElement: ElementWithOpId, autofillField: AutofillField, pageDetails?: AutofillPageDetails, @@ -1468,7 +1432,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte this.getFormattedAutofillFieldsData(), ); - void this.autofillOverlayContentService.setupAutofillOverlayListenerOnField( + void this.autofillOverlayContentService.setupInlineMenu( formFieldElement, autofillField, autofillPageDetails, diff --git a/apps/browser/src/autofill/services/dom-element-visibility.service.ts b/apps/browser/src/autofill/services/dom-element-visibility.service.ts index 127ce84d919..67986eb00f2 100644 --- a/apps/browser/src/autofill/services/dom-element-visibility.service.ts +++ b/apps/browser/src/autofill/services/dom-element-visibility.service.ts @@ -1,3 +1,4 @@ +import { AutofillInlineMenuContentService } from "../overlay/inline-menu/abstractions/autofill-inline-menu-content.service"; import { FillableFormFieldElement, FormFieldElement } from "../types"; import { DomElementVisibilityService as domElementVisibilityServiceInterface } from "./abstractions/dom-element-visibility.service"; @@ -5,6 +6,8 @@ import { DomElementVisibilityService as domElementVisibilityServiceInterface } f class DomElementVisibilityService implements domElementVisibilityServiceInterface { private cachedComputedStyle: CSSStyleDeclaration | null = null; + constructor(private inlineMenuElements?: AutofillInlineMenuContentService) {} + /** * Checks if a form field is viewable. This is done by checking if the element is within the * viewport bounds, not hidden by CSS, and not hidden behind another element. @@ -187,6 +190,10 @@ class DomElementVisibilityService implements domElementVisibilityServiceInterfac return true; } + if (this.inlineMenuElements?.isElementInlineMenu(elementAtCenterPoint as HTMLElement)) { + return true; + } + const targetElementLabelsSet = new Set((targetElement as FillableFormFieldElement).labels); if (targetElementLabelsSet.has(elementAtCenterPoint as HTMLLabelElement)) { return true; diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts index 7bc027b392c..a6253dffac2 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts @@ -2,11 +2,11 @@ import AutofillField from "../models/autofill-field"; import AutofillPageDetails from "../models/autofill-page-details"; import { sendExtensionMessage } from "../utils"; -import { InlineMenuFieldQualificationsService as InlineMenuFieldQualificationsServiceInterface } from "./abstractions/inline-menu-field-qualifications.service"; +import { InlineMenuFieldQualificationService as InlineMenuFieldQualificationServiceInterface } from "./abstractions/inline-menu-field-qualifications.service"; import { AutoFillConstants } from "./autofill-constants"; export class InlineMenuFieldQualificationService - implements InlineMenuFieldQualificationsServiceInterface + implements InlineMenuFieldQualificationServiceInterface { private searchFieldNamesSet = new Set(AutoFillConstants.SearchFieldNames); private excludedAutofillLoginTypesSet = new Set(AutoFillConstants.ExcludedAutofillLoginTypes); diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts index 6ee5171e58c..ff0e82d664d 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts @@ -1,10 +1,13 @@ +import { mock } from "jest-mock-extended"; + import { EVENTS } from "@bitwarden/common/autofill/constants"; import AutofillScript, { FillScript, FillScriptActions } from "../models/autofill-script"; import { mockQuerySelectorAllDefinedCall } from "../spec/testing-utils"; import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types"; -import AutofillOverlayContentService from "./autofill-overlay-content.service"; +import { InlineMenuFieldQualificationService } from "./abstractions/inline-menu-field-qualifications.service"; +import { AutofillOverlayContentService } from "./autofill-overlay-content.service"; import CollectAutofillContentService from "./collect-autofill-content.service"; import DomElementVisibilityService from "./dom-element-visibility.service"; import InsertAutofillContentService from "./insert-autofill-content.service"; @@ -64,8 +67,11 @@ function setMockWindowLocation({ } describe("InsertAutofillContentService", () => { + const inlineMenuFieldQualificationService = mock(); const domElementVisibilityService = new DomElementVisibilityService(); - const autofillOverlayContentService = new AutofillOverlayContentService(); + const autofillOverlayContentService = new AutofillOverlayContentService( + inlineMenuFieldQualificationService, + ); const collectAutofillContentService = new CollectAutofillContentService( domElementVisibilityService, autofillOverlayContentService, diff --git a/apps/browser/src/autofill/spec/autofill-mocks.ts b/apps/browser/src/autofill/spec/autofill-mocks.ts index 021b7719b2b..2d4ffd7f217 100644 --- a/apps/browser/src/autofill/spec/autofill-mocks.ts +++ b/apps/browser/src/autofill/spec/autofill-mocks.ts @@ -7,16 +7,16 @@ 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 { OverlayCipherData } from "../background/abstractions/overlay.background"; +import { InlineMenuCipherData } from "../background/abstractions/overlay.background"; import AutofillField from "../models/autofill-field"; import AutofillForm from "../models/autofill-form"; import AutofillPageDetails from "../models/autofill-page-details"; import AutofillScript, { FillScript } from "../models/autofill-script"; -import { InitAutofillOverlayButtonMessage } from "../overlay/abstractions/autofill-overlay-button"; -import { InitAutofillOverlayListMessage } from "../overlay/abstractions/autofill-overlay-list"; +import { InitAutofillInlineMenuButtonMessage } from "../overlay/inline-menu/abstractions/autofill-inline-menu-button"; +import { InitAutofillInlineMenuListMessage } from "../overlay/inline-menu/abstractions/autofill-inline-menu-list"; import { GenerateFillScriptOptions, PageDetail } from "../services/abstractions/autofill.service"; -function createAutofillFormMock(customFields = {}): AutofillForm { +export function createAutofillFormMock(customFields = {}): AutofillForm { return { opid: "default-form-opid", htmlID: "default-htmlID", @@ -27,7 +27,7 @@ function createAutofillFormMock(customFields = {}): AutofillForm { }; } -function createAutofillFieldMock(customFields = {}): AutofillField { +export function createAutofillFieldMock(customFields = {}): AutofillField { return { opid: "default-input-field-opid", elementNumber: 0, @@ -57,7 +57,7 @@ function createAutofillFieldMock(customFields = {}): AutofillField { }; } -function createPageDetailMock(customFields = {}): PageDetail { +export function createPageDetailMock(customFields = {}): PageDetail { return { frameId: 0, tab: createChromeTabMock(), @@ -66,7 +66,7 @@ function createPageDetailMock(customFields = {}): PageDetail { }; } -function createAutofillPageDetailsMock(customFields = {}): AutofillPageDetails { +export function createAutofillPageDetailsMock(customFields = {}): AutofillPageDetails { return { title: "title", url: "url", @@ -86,7 +86,7 @@ function createAutofillPageDetailsMock(customFields = {}): AutofillPageDetails { }; } -function createChromeTabMock(customFields = {}): chrome.tabs.Tab { +export function createChromeTabMock(customFields = {}): chrome.tabs.Tab { return { id: 1, index: 1, @@ -104,7 +104,7 @@ function createChromeTabMock(customFields = {}): chrome.tabs.Tab { }; } -function createGenerateFillScriptOptionsMock(customFields = {}): GenerateFillScriptOptions { +export function createGenerateFillScriptOptionsMock(customFields = {}): GenerateFillScriptOptions { return { skipUsernameOnlyFill: false, onlyEmptyFields: false, @@ -118,7 +118,7 @@ function createGenerateFillScriptOptionsMock(customFields = {}): GenerateFillScr }; } -function createAutofillScriptMock( +export function createAutofillScriptMock( customFields = {}, scriptTypes?: Record, ): AutofillScript { @@ -159,24 +159,28 @@ const overlayPagesTranslations = { unlockYourAccount: "unlockYourAccount", unlockAccount: "unlockAccount", fillCredentialsFor: "fillCredentialsFor", - partialUsername: "partialUsername", + username: "username", view: "view", noItemsToShow: "noItemsToShow", newItem: "newItem", addNewVaultItem: "addNewVaultItem", }; -function createInitAutofillOverlayButtonMessageMock( +export function createInitAutofillInlineMenuButtonMessageMock( customFields = {}, -): InitAutofillOverlayButtonMessage { +): InitAutofillInlineMenuButtonMessage { return { - command: "initAutofillOverlayButton", + command: "initAutofillInlineMenuButton", translations: overlayPagesTranslations, styleSheetUrl: "https://jest-testing-website.com", authStatus: AuthenticationStatus.Unlocked, + portKey: "portKey", ...customFields, }; } -function createAutofillOverlayCipherDataMock(index: number, customFields = {}): OverlayCipherData { +export function createAutofillOverlayCipherDataMock( + index: number, + customFields = {}, +): InlineMenuCipherData { return { id: String(index), name: `website login ${index}`, @@ -194,15 +198,16 @@ function createAutofillOverlayCipherDataMock(index: number, customFields = {}): }; } -function createInitAutofillOverlayListMessageMock( +export function createInitAutofillInlineMenuListMessageMock( customFields = {}, -): InitAutofillOverlayListMessage { +): InitAutofillInlineMenuListMessage { return { - command: "initAutofillOverlayList", + command: "initAutofillInlineMenuList", translations: overlayPagesTranslations, styleSheetUrl: "https://jest-testing-website.com", theme: ThemeType.Light, authStatus: AuthenticationStatus.Unlocked, + portKey: "portKey", ciphers: [ createAutofillOverlayCipherDataMock(1, { icon: { @@ -237,7 +242,7 @@ function createInitAutofillOverlayListMessageMock( }; } -function createFocusedFieldDataMock(customFields = {}) { +export function createFocusedFieldDataMock(customFields = {}) { return { focusedFieldRects: { top: 1, @@ -250,11 +255,12 @@ function createFocusedFieldDataMock(customFields = {}) { paddingLeft: "6px", }, tabId: 1, + frameId: 2, ...customFields, }; } -function createPortSpyMock(name: string) { +export function createPortSpyMock(name: string) { return mock({ name, onMessage: { @@ -273,16 +279,17 @@ function createPortSpyMock(name: string) { }); } -export { - createAutofillFormMock, - createAutofillFieldMock, - createPageDetailMock, - createAutofillPageDetailsMock, - createChromeTabMock, - createGenerateFillScriptOptionsMock, - createAutofillScriptMock, - createInitAutofillOverlayButtonMessageMock, - createInitAutofillOverlayListMessageMock, - createFocusedFieldDataMock, - createPortSpyMock, -}; +export function createMutationRecordMock(customFields = {}): MutationRecord { + return { + addedNodes: mock(), + attributeName: "default-attributeName", + attributeNamespace: "default-attributeNamespace", + nextSibling: null, + oldValue: "default-oldValue", + previousSibling: null, + removedNodes: mock(), + target: null, + type: "attributes", + ...customFields, + }; +} diff --git a/apps/browser/src/autofill/spec/testing-utils.ts b/apps/browser/src/autofill/spec/testing-utils.ts index 5b0db5ebd6f..1cef5186028 100644 --- a/apps/browser/src/autofill/spec/testing-utils.ts +++ b/apps/browser/src/autofill/spec/testing-utils.ts @@ -1,21 +1,21 @@ import { mock } from "jest-mock-extended"; -function triggerTestFailure() { +export function triggerTestFailure() { expect(true).toBe("Test has failed."); } const scheduler = typeof setImmediate === "function" ? setImmediate : setTimeout; -function flushPromises() { +export function flushPromises() { return new Promise(function (resolve) { scheduler(resolve); }); } -function postWindowMessage(data: any, origin = "https://localhost/", source = window) { +export function postWindowMessage(data: any, origin = "https://localhost/", source = window) { globalThis.dispatchEvent(new MessageEvent("message", { data, origin, source })); } -function sendMockExtensionMessage( +export function sendMockExtensionMessage( message: any, sender?: chrome.runtime.MessageSender, sendResponse?: CallableFunction, @@ -32,7 +32,7 @@ function sendMockExtensionMessage( ); } -function triggerRuntimeOnConnectEvent(port: chrome.runtime.Port) { +export function triggerRuntimeOnConnectEvent(port: chrome.runtime.Port) { (chrome.runtime.onConnect.addListener as unknown as jest.SpyInstance).mock.calls.forEach( (call) => { const callback = call[0]; @@ -41,21 +41,37 @@ function triggerRuntimeOnConnectEvent(port: chrome.runtime.Port) { ); } -function sendPortMessage(port: chrome.runtime.Port, message: any) { +export function sendPortMessage(port: chrome.runtime.Port, message: any) { (port.onMessage.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => { const callback = call[0]; callback(message || {}, port); }); } -function triggerPortOnDisconnectEvent(port: chrome.runtime.Port) { +export function triggerPortOnConnectEvent(port: chrome.runtime.Port) { + (chrome.runtime.onConnect.addListener as unknown as jest.SpyInstance).mock.calls.forEach( + (call) => { + const callback = call[0]; + callback(port); + }, + ); +} + +export function triggerPortOnMessageEvent(port: chrome.runtime.Port, message: any) { + (port.onMessage.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => { + const callback = call[0]; + callback(message, port); + }); +} + +export function triggerPortOnDisconnectEvent(port: chrome.runtime.Port) { (port.onDisconnect.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => { const callback = call[0]; callback(port); }); } -function triggerWindowOnFocusedChangedEvent(windowId: number) { +export function triggerWindowOnFocusedChangedEvent(windowId: number) { (chrome.windows.onFocusChanged.addListener as unknown as jest.SpyInstance).mock.calls.forEach( (call) => { const callback = call[0]; @@ -64,7 +80,7 @@ function triggerWindowOnFocusedChangedEvent(windowId: number) { ); } -function triggerTabOnActivatedEvent(activeInfo: chrome.tabs.TabActiveInfo) { +export function triggerTabOnActivatedEvent(activeInfo: chrome.tabs.TabActiveInfo) { (chrome.tabs.onActivated.addListener as unknown as jest.SpyInstance).mock.calls.forEach( (call) => { const callback = call[0]; @@ -73,14 +89,14 @@ function triggerTabOnActivatedEvent(activeInfo: chrome.tabs.TabActiveInfo) { ); } -function triggerTabOnReplacedEvent(addedTabId: number, removedTabId: number) { +export function triggerTabOnReplacedEvent(addedTabId: number, removedTabId: number) { (chrome.tabs.onReplaced.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => { const callback = call[0]; callback(addedTabId, removedTabId); }); } -function triggerTabOnUpdatedEvent( +export function triggerTabOnUpdatedEvent( tabId: number, changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab, @@ -91,14 +107,32 @@ function triggerTabOnUpdatedEvent( }); } -function triggerTabOnRemovedEvent(tabId: number, removeInfo: chrome.tabs.TabRemoveInfo) { +export function triggerTabOnRemovedEvent(tabId: number, removeInfo: chrome.tabs.TabRemoveInfo) { (chrome.tabs.onRemoved.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => { const callback = call[0]; callback(tabId, removeInfo); }); } -function mockQuerySelectorAllDefinedCall() { +export function triggerOnAlarmEvent(alarm: chrome.alarms.Alarm) { + (chrome.alarms.onAlarm.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => { + const callback = call[0]; + callback(alarm); + }); +} + +export function triggerWebNavigationOnCommittedEvent( + details: chrome.webNavigation.WebNavigationFramedCallbackDetails, +) { + (chrome.webNavigation.onCommitted.addListener as unknown as jest.SpyInstance).mock.calls.forEach( + (call) => { + const callback = call[0]; + callback(details); + }, + ); +} + +export function mockQuerySelectorAllDefinedCall() { const originalDocumentQuerySelectorAll = document.querySelectorAll; document.querySelectorAll = function (selector: string) { return originalDocumentQuerySelectorAll.call( @@ -125,19 +159,3 @@ function mockQuerySelectorAllDefinedCall() { }, }; } - -export { - triggerTestFailure, - flushPromises, - postWindowMessage, - sendMockExtensionMessage, - triggerRuntimeOnConnectEvent, - sendPortMessage, - triggerPortOnDisconnectEvent, - triggerWindowOnFocusedChangedEvent, - triggerTabOnActivatedEvent, - triggerTabOnReplacedEvent, - triggerTabOnUpdatedEvent, - triggerTabOnRemovedEvent, - mockQuerySelectorAllDefinedCall, -}; diff --git a/apps/browser/src/autofill/utils/autofill-overlay.enum.ts b/apps/browser/src/autofill/utils/autofill-overlay.enum.ts deleted file mode 100644 index 486d68f7540..00000000000 --- a/apps/browser/src/autofill/utils/autofill-overlay.enum.ts +++ /dev/null @@ -1,17 +0,0 @@ -const AutofillOverlayElement = { - Button: "autofill-overlay-button", - List: "autofill-overlay-list", -} as const; - -const AutofillOverlayPort = { - Button: "autofill-overlay-button-port", - List: "autofill-overlay-list-port", -} as const; - -const RedirectFocusDirection = { - Current: "current", - Previous: "previous", - Next: "next", -} as const; - -export { AutofillOverlayElement, AutofillOverlayPort, RedirectFocusDirection }; diff --git a/apps/browser/src/autofill/utils/index.spec.ts b/apps/browser/src/autofill/utils/index.spec.ts index dcb5aa64696..116df044b37 100644 --- a/apps/browser/src/autofill/utils/index.spec.ts +++ b/apps/browser/src/autofill/utils/index.spec.ts @@ -1,4 +1,4 @@ -import { AutofillPort } from "../enums/autofill-port.enums"; +import { AutofillPort } from "../enums/autofill-port.enum"; import { triggerPortOnDisconnectEvent } from "../spec/testing-utils"; import { logoIcon, logoLockedIcon } from "./svg-icons"; @@ -38,9 +38,7 @@ describe("generateRandomCustomElementName", () => { describe("sendExtensionMessage", () => { it("sends a message to the extension", async () => { - const extensionMessagePromise = sendExtensionMessage("updateAutofillOverlayHidden", { - display: "none", - }); + const extensionMessagePromise = sendExtensionMessage("some-extension-message"); // Jest doesn't give anyway to select the typed overload of "sendMessage", // a cast is needed to get the correct spy type. diff --git a/apps/browser/src/autofill/utils/index.ts b/apps/browser/src/autofill/utils/index.ts index 873012d1dbb..a040fa50122 100644 --- a/apps/browser/src/autofill/utils/index.ts +++ b/apps/browser/src/autofill/utils/index.ts @@ -1,5 +1,24 @@ -import { AutofillPort } from "../enums/autofill-port.enums"; -import { FillableFormFieldElement, FormFieldElement } from "../types"; +import { AutofillPort } from "../enums/autofill-port.enum"; +import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types"; + +/** + * Generates a random string of characters. + * + * @param length - The length of the random string to generate. + */ +export function generateRandomChars(length: number): string { + const chars = "abcdefghijklmnopqrstuvwxyz"; + const randomChars = []; + const randomBytes = new Uint8Array(length); + globalThis.crypto.getRandomValues(randomBytes); + + for (let byteIndex = 0; byteIndex < randomBytes.length; byteIndex++) { + const byte = randomBytes[byteIndex]; + randomChars.push(chars[byte % chars.length]); + } + + return randomChars.join(""); +} /** * Polyfills the requestIdleCallback API with a setTimeout fallback. @@ -34,21 +53,7 @@ export function cancelIdleCallbackPolyfill(id: NodeJS.Timeout | number) { /** * Generates a random string of characters that formatted as a custom element name. */ -function generateRandomCustomElementName(): string { - const generateRandomChars = (length: number): string => { - const chars = "abcdefghijklmnopqrstuvwxyz"; - const randomChars = []; - const randomBytes = new Uint8Array(length); - globalThis.crypto.getRandomValues(randomBytes); - - for (let byteIndex = 0; byteIndex < randomBytes.length; byteIndex++) { - const byte = randomBytes[byteIndex]; - randomChars.push(chars[byte % chars.length]); - } - - return randomChars.join(""); - }; - +export function generateRandomCustomElementName(): string { const length = Math.floor(Math.random() * 5) + 8; // Between 8 and 12 characters const numHyphens = Math.min(Math.max(Math.floor(Math.random() * 4), 1), length - 1); // At least 1, maximum of 3 hyphens @@ -81,7 +86,7 @@ function generateRandomCustomElementName(): string { * @param svgString - The SVG string to build the DOM element from. * @param ariaHidden - Determines whether the SVG should be hidden from screen readers. */ -function buildSvgDomElement(svgString: string, ariaHidden = true): HTMLElement { +export function buildSvgDomElement(svgString: string, ariaHidden = true): HTMLElement { const domParser = new DOMParser(); const svgDom = domParser.parseFromString(svgString, "image/svg+xml"); const domElement = svgDom.documentElement; @@ -96,14 +101,14 @@ function buildSvgDomElement(svgString: string, ariaHidden = true): HTMLElement { * @param command - The command to send. * @param options - The options to send with the command. */ -async function sendExtensionMessage( +export async function sendExtensionMessage( command: string, options: Record = {}, ): Promise { return new Promise((resolve) => { chrome.runtime.sendMessage(Object.assign({ command }, options), (response) => { if (chrome.runtime.lastError) { - return; + // Do nothing } resolve(response); @@ -118,7 +123,7 @@ async function sendExtensionMessage( * @param styles - The styles to set on the element. * @param priority - Determines whether the styles should be set as important. */ -function setElementStyles( +export function setElementStyles( element: HTMLElement, styles: Partial, priority?: boolean, @@ -141,9 +146,9 @@ function setElementStyles( * and triggers an onDisconnect event if the extension context * is invalidated. * - * @param callback - Callback function to run when the extension disconnects + * @param callback - Callback export function to run when the extension disconnects */ -function setupExtensionDisconnectAction(callback: (port: chrome.runtime.Port) => void) { +export function setupExtensionDisconnectAction(callback: (port: chrome.runtime.Port) => void) { const port = chrome.runtime.connect({ name: AutofillPort.InjectedScript }); const onDisconnectCallback = (disconnectedPort: chrome.runtime.Port) => { callback(disconnectedPort); @@ -158,7 +163,7 @@ function setupExtensionDisconnectAction(callback: (port: chrome.runtime.Port) => * * @param windowContext - The global window context */ -function setupAutofillInitDisconnectAction(windowContext: Window) { +export function setupAutofillInitDisconnectAction(windowContext: Window) { if (!windowContext.bitwardenAutofillInit) { return; } @@ -176,10 +181,10 @@ function setupAutofillInitDisconnectAction(windowContext: Window) { * * @param formFieldElement - The form field element to check. */ -function elementIsFillableFormField( +export function elementIsFillableFormField( formFieldElement: FormFieldElement, ): formFieldElement is FillableFormFieldElement { - return formFieldElement?.tagName.toLowerCase() !== "span"; + return !elementIsSpanElement(formFieldElement); } /** @@ -188,8 +193,11 @@ function elementIsFillableFormField( * @param element - The element to check. * @param tagName - The tag name to check against. */ -function elementIsInstanceOf(element: Element, tagName: string): element is T { - return element?.tagName.toLowerCase() === tagName; +export function elementIsInstanceOf( + element: Element, + tagName: string, +): element is T { + return nodeIsElement(element) && element.tagName.toLowerCase() === tagName; } /** @@ -197,7 +205,7 @@ function elementIsInstanceOf(element: Element, tagName: strin * * @param element - The element to check. */ -function elementIsSpanElement(element: Element): element is HTMLSpanElement { +export function elementIsSpanElement(element: Element): element is HTMLSpanElement { return elementIsInstanceOf(element, "span"); } @@ -206,7 +214,7 @@ function elementIsSpanElement(element: Element): element is HTMLSpanElement { * * @param element - The element to check. */ -function elementIsInputElement(element: Element): element is HTMLInputElement { +export function elementIsInputElement(element: Element): element is HTMLInputElement { return elementIsInstanceOf(element, "input"); } @@ -215,7 +223,7 @@ function elementIsInputElement(element: Element): element is HTMLInputElement { * * @param element - The element to check. */ -function elementIsSelectElement(element: Element): element is HTMLSelectElement { +export function elementIsSelectElement(element: Element): element is HTMLSelectElement { return elementIsInstanceOf(element, "select"); } @@ -224,7 +232,7 @@ function elementIsSelectElement(element: Element): element is HTMLSelectElement * * @param element - The element to check. */ -function elementIsTextAreaElement(element: Element): element is HTMLTextAreaElement { +export function elementIsTextAreaElement(element: Element): element is HTMLTextAreaElement { return elementIsInstanceOf(element, "textarea"); } @@ -233,7 +241,7 @@ function elementIsTextAreaElement(element: Element): element is HTMLTextAreaElem * * @param element - The element to check. */ -function elementIsFormElement(element: Element): element is HTMLFormElement { +export function elementIsFormElement(element: Element): element is HTMLFormElement { return elementIsInstanceOf(element, "form"); } @@ -242,7 +250,7 @@ function elementIsFormElement(element: Element): element is HTMLFormElement { * * @param element - The element to check. */ -function elementIsLabelElement(element: Element): element is HTMLLabelElement { +export function elementIsLabelElement(element: Element): element is HTMLLabelElement { return elementIsInstanceOf(element, "label"); } @@ -251,7 +259,7 @@ function elementIsLabelElement(element: Element): element is HTMLLabelElement { * * @param element - The element to check. */ -function elementIsDescriptionDetailsElement(element: Element): element is HTMLElement { +export function elementIsDescriptionDetailsElement(element: Element): element is HTMLElement { return elementIsInstanceOf(element, "dd"); } @@ -260,7 +268,7 @@ function elementIsDescriptionDetailsElement(element: Element): element is HTMLEl * * @param element - The element to check. */ -function elementIsDescriptionTermElement(element: Element): element is HTMLElement { +export function elementIsDescriptionTermElement(element: Element): element is HTMLElement { return elementIsInstanceOf(element, "dt"); } @@ -269,12 +277,12 @@ function elementIsDescriptionTermElement(element: Element): element is HTMLEleme * * @param node - The node to check. */ -function nodeIsElement(node: Node): node is Element { +export function nodeIsElement(node: Node): node is Element { if (!node) { return false; } - return node.nodeType === Node.ELEMENT_NODE; + return node?.nodeType === Node.ELEMENT_NODE; } /** @@ -282,7 +290,7 @@ function nodeIsElement(node: Node): node is Element { * * @param node - The node to check. */ -function nodeIsInputElement(node: Node): node is HTMLInputElement { +export function nodeIsInputElement(node: Node): node is HTMLInputElement { return nodeIsElement(node) && elementIsInputElement(node); } @@ -291,28 +299,56 @@ function nodeIsInputElement(node: Node): node is HTMLInputElement { * * @param node - The node to check. */ -function nodeIsFormElement(node: Node): node is HTMLFormElement { +export function nodeIsFormElement(node: Node): node is HTMLFormElement { return nodeIsElement(node) && elementIsFormElement(node); } -export { - generateRandomCustomElementName, - buildSvgDomElement, - sendExtensionMessage, - setElementStyles, - setupExtensionDisconnectAction, - setupAutofillInitDisconnectAction, - elementIsFillableFormField, - elementIsInstanceOf, - elementIsSpanElement, - elementIsInputElement, - elementIsSelectElement, - elementIsTextAreaElement, - elementIsFormElement, - elementIsLabelElement, - elementIsDescriptionDetailsElement, - elementIsDescriptionTermElement, - nodeIsElement, - nodeIsInputElement, - nodeIsFormElement, -}; +/** + * Returns a boolean representing the attribute value of an element. + * + * @param element + * @param attributeName + * @param checkString + */ +export function getAttributeBoolean( + element: HTMLElement, + attributeName: string, + checkString = false, +): boolean { + if (checkString) { + return getPropertyOrAttribute(element, attributeName) === "true"; + } + + return Boolean(getPropertyOrAttribute(element, attributeName)); +} + +/** + * Get the value of a property or attribute from a FormFieldElement. + * + * @param element + * @param attributeName + */ +export function getPropertyOrAttribute(element: HTMLElement, attributeName: string): string | null { + if (attributeName in element) { + return (element as FormElementWithAttribute)[attributeName]; + } + + return element.getAttribute(attributeName); +} + +/** + * Throttles a callback function to run at most once every `limit` milliseconds. + * + * @param callback - The callback function to throttle. + * @param limit - The time in milliseconds to throttle the callback. + */ +export function throttle(callback: () => void, limit: number) { + let waitingDelay = false; + return function (...args: unknown[]) { + if (!waitingDelay) { + callback.apply(this, args); + waitingDelay = true; + globalThis.setTimeout(() => (waitingDelay = false), limit); + } + }; +} diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index d438bced4b3..c271dd29db3 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -72,6 +72,7 @@ import { import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; import { ClientType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -105,12 +106,15 @@ import { clearCaches } from "@bitwarden/common/platform/misc/sequentialize"; import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { BulkEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/bulk-encrypt.service.implementation"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; +import { FallbackBulkEncryptService } from "@bitwarden/common/platform/services/cryptography/fallback-bulk-encrypt.service"; import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; import { Fido2AuthenticatorService } from "@bitwarden/common/platform/services/fido2/fido2-authenticator.service"; import { Fido2ClientService } from "@bitwarden/common/platform/services/fido2/fido2-client.service"; @@ -196,14 +200,16 @@ import { VaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; +import { OverlayBackground as OverlayBackgroundInterface } from "../autofill/background/abstractions/overlay.background"; import ContextMenusBackground from "../autofill/background/context-menus.background"; import NotificationBackground from "../autofill/background/notification.background"; -import OverlayBackground from "../autofill/background/overlay.background"; +import { OverlayBackground } from "../autofill/background/overlay.background"; import TabsBackground from "../autofill/background/tabs.background"; import WebRequestBackground from "../autofill/background/web-request.background"; import { CipherContextMenuHandler } from "../autofill/browser/cipher-context-menu-handler"; import { ContextMenuClickedHandler } from "../autofill/browser/context-menu-clicked-handler"; import { MainContextMenuHandler } from "../autofill/browser/main-context-menu-handler"; +import LegacyOverlayBackground from "../autofill/deprecated/background/overlay.background.deprecated"; import { Fido2Background as Fido2BackgroundAbstraction } from "../autofill/fido2/background/abstractions/fido2.background"; import { Fido2Background } from "../autofill/fido2/background/fido2.background"; import { AutofillService as AutofillServiceAbstraction } from "../autofill/services/abstractions/autofill.service"; @@ -216,6 +222,7 @@ import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender /* eslint-enable no-restricted-imports */ import { OffscreenDocumentService } from "../platform/offscreen-document/abstractions/offscreen-document"; import { DefaultOffscreenDocumentService } from "../platform/offscreen-document/offscreen-document.service"; +import { BrowserTaskSchedulerService } from "../platform/services/abstractions/browser-task-scheduler.service"; import { BrowserCryptoService } from "../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../platform/services/browser-local-storage.service"; @@ -225,6 +232,8 @@ import I18nService from "../platform/services/i18n.service"; import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service"; import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; +import { BackgroundTaskSchedulerService } from "../platform/services/task-scheduler/background-task-scheduler.service"; +import { ForegroundTaskSchedulerService } from "../platform/services/task-scheduler/foreground-task-scheduler.service"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; import { BrowserStorageServiceProvider } from "../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../platform/storage/foreground-memory-storage.service"; @@ -240,7 +249,6 @@ import CommandsBackground from "./commands.background"; import IdleBackground from "./idle.background"; import { NativeMessagingBackground } from "./nativeMessaging.background"; import RuntimeBackground from "./runtime.background"; - export default class MainBackground { messagingService: MessageSender; storageService: BrowserLocalStorageService; @@ -299,6 +307,7 @@ export default class MainBackground { vaultFilterService: VaultFilterService; usernameGenerationService: UsernameGenerationServiceAbstraction; encryptService: EncryptService; + bulkEncryptService: FallbackBulkEncryptService; folderApiService: FolderApiServiceAbstraction; policyApiService: PolicyApiServiceAbstraction; sendApiService: SendApiServiceAbstraction; @@ -322,6 +331,7 @@ export default class MainBackground { activeUserStateProvider: ActiveUserStateProvider; derivedStateProvider: DerivedStateProvider; stateProvider: StateProvider; + taskSchedulerService: BrowserTaskSchedulerService; fido2Background: Fido2BackgroundAbstraction; individualVaultExportService: IndividualVaultExportServiceAbstraction; organizationVaultExportService: OrganizationVaultExportServiceAbstraction; @@ -346,7 +356,7 @@ export default class MainBackground { private contextMenusBackground: ContextMenusBackground; private idleBackground: IdleBackground; private notificationBackground: NotificationBackground; - private overlayBackground: OverlayBackground; + private overlayBackground: OverlayBackgroundInterface; private filelessImporterBackground: FilelessImporterBackground; private runtimeBackground: RuntimeBackground; private tabsBackground: TabsBackground; @@ -511,6 +521,14 @@ export default class MainBackground { this.globalStateProvider, this.derivedStateProvider, ); + + this.taskSchedulerService = this.popupOnlyContext + ? new ForegroundTaskSchedulerService(this.logService, this.stateProvider) + : new BackgroundTaskSchedulerService(this.logService, this.stateProvider); + this.taskSchedulerService.registerTaskHandler(ScheduledTaskNames.scheduleNextSyncInterval, () => + this.fullSync(), + ); + this.environmentService = new BrowserEnvironmentService( this.logService, this.stateProvider, @@ -716,6 +734,7 @@ export default class MainBackground { this.environmentService, this.logService, this.stateProvider, + this.authService, ); this.cipherService = new CipherService( @@ -727,6 +746,7 @@ export default class MainBackground { this.stateService, this.autofillSettingsService, this.encryptService, + this.bulkEncryptService, this.cipherFileUploadService, this.configService, this.stateProvider, @@ -778,6 +798,8 @@ export default class MainBackground { this.authService, this.vaultTimeoutSettingsService, this.stateEventRunnerService, + this.taskSchedulerService, + this.logService, lockedCallback, logoutCallback, ); @@ -857,6 +879,7 @@ export default class MainBackground { this.stateProvider, this.logService, this.authService, + this.taskSchedulerService, ); this.eventCollectionService = new EventCollectionService( this.cipherService, @@ -884,6 +907,7 @@ export default class MainBackground { this.scriptInjectorService, this.accountService, this.authService, + this.configService, messageListener, ); this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); @@ -934,6 +958,7 @@ export default class MainBackground { this.stateService, this.authService, this.messagingService, + this.taskSchedulerService, ); this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.authService); @@ -949,16 +974,17 @@ export default class MainBackground { this.authService, this.vaultSettingsService, this.domainSettingsService, + this.taskSchedulerService, this.logService, ); - const systemUtilsServiceReloadCallback = () => { + const systemUtilsServiceReloadCallback = async () => { const forceWindowReload = this.platformUtilsService.isSafari() || this.platformUtilsService.isFirefox() || this.platformUtilsService.isOpera(); + await this.taskSchedulerService.clearAllScheduledTasks(); BrowserApi.reloadExtension(forceWindowReload ? self : null); - return Promise.resolve(); }; this.systemService = new SystemService( @@ -970,6 +996,7 @@ export default class MainBackground { this.vaultTimeoutSettingsService, this.biometricStateService, this.accountService, + this.taskSchedulerService, ); // Other fields @@ -1032,17 +1059,7 @@ export default class MainBackground { themeStateService, this.configService, ); - this.overlayBackground = new OverlayBackground( - this.cipherService, - this.autofillService, - this.authService, - this.environmentService, - this.domainSettingsService, - this.autofillSettingsService, - this.i18nService, - this.platformUtilsService, - themeStateService, - ); + this.filelessImporterBackground = new FilelessImporterBackground( this.configService, this.authService, @@ -1052,11 +1069,6 @@ export default class MainBackground { this.syncService, this.scriptInjectorService, ); - this.tabsBackground = new TabsBackground( - this, - this.notificationBackground, - this.overlayBackground, - ); const contextMenuClickedHandler = new ContextMenuClickedHandler( (options) => this.platformUtilsService.copyToClipboard(options.text), @@ -1136,6 +1148,47 @@ export default class MainBackground { } this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService); + + this.configService + .getFeatureFlag(FeatureFlag.InlineMenuPositioningImprovements) + .then(async (enabled) => { + if (!enabled) { + this.overlayBackground = new LegacyOverlayBackground( + this.cipherService, + this.autofillService, + this.authService, + this.environmentService, + this.domainSettingsService, + this.autofillSettingsService, + this.i18nService, + this.platformUtilsService, + themeStateService, + ); + } else { + this.overlayBackground = new OverlayBackground( + this.logService, + this.cipherService, + this.autofillService, + this.authService, + this.environmentService, + this.domainSettingsService, + this.autofillSettingsService, + this.i18nService, + this.platformUtilsService, + themeStateService, + ); + } + + this.tabsBackground = new TabsBackground( + this, + this.notificationBackground, + this.overlayBackground, + ); + + await this.overlayBackground.init(); + await this.tabsBackground.init(); + }) + .catch((error) => this.logService.error(`Error initializing OverlayBackground: ${error}`)); } async bootstrap() { @@ -1172,18 +1225,30 @@ export default class MainBackground { await this.notificationBackground.init(); this.filelessImporterBackground.init(); await this.commandsBackground.init(); - await this.overlayBackground.init(); - await this.tabsBackground.init(); this.contextMenusBackground?.init(); await this.idleBackground.init(); this.webRequestBackground?.startListening(); this.syncServiceListener?.listener$().subscribe(); + if ( + BrowserApi.isManifestVersion(2) && + (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) + ) { + await this.bulkEncryptService.setFeatureFlagEncryptService( + new BulkEncryptServiceImplementation(this.cryptoFunctionService, this.logService), + ); + } + return new Promise((resolve) => { setTimeout(async () => { await this.refreshBadge(); await this.fullSync(true); + await this.taskSchedulerService.setInterval( + ScheduledTaskNames.scheduleNextSyncInterval, + 5 * 60 * 1000, // check every 5 minutes + ); setTimeout(() => this.notificationsService.init(), 2500); + await this.taskSchedulerService.verifyAlarmsState(); resolve(); }, 500); }); @@ -1452,17 +1517,6 @@ export default class MainBackground { if (override || lastSyncAgo >= syncInternal) { await this.syncService.fullSync(override); - this.scheduleNextSync(); - } else { - this.scheduleNextSync(); } } - - private scheduleNextSync() { - if (this.syncTimeout) { - clearTimeout(this.syncTimeout); - } - - this.syncTimeout = setTimeout(async () => await this.fullSync(), 5 * 60 * 1000); // check every 5 minutes - } } diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 94e96e2dc89..ccf82057449 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -233,6 +233,9 @@ export default class RuntimeBackground { case "addToLockedVaultPendingNotifications": this.lockedVaultPendingNotifications.push(msg.data); break; + case "lockVault": + await this.main.vaultTimeoutService.lock(msg.userId); + break; case "logout": await this.main.logout(msg.expired, msg.userId); break; diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index b1c51911ec8..35eb3daebfe 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.7.0", + "version": "2024.7.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", @@ -59,6 +59,7 @@ "clipboardRead", "clipboardWrite", "idle", + "alarms", "webRequest", "webRequestBlocking", "webNavigation" @@ -66,7 +67,12 @@ "optional_permissions": ["nativeMessaging", "privacy"], "content_security_policy": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'", "sandbox": { - "pages": ["overlay/button.html", "overlay/list.html"], + "pages": [ + "overlay/menu-button.html", + "overlay/menu-list.html", + "overlay/button.html", + "overlay/list.html" + ], "content_security_policy": "sandbox allow-scripts; script-src 'self'" }, "commands": { @@ -106,6 +112,9 @@ "notification/bar.html", "images/icon38.png", "images/icon38_locked.png", + "overlay/menu-button.html", + "overlay/menu-list.html", + "overlay/menu.html", "overlay/button.html", "overlay/list.html", "popup/fonts/*" diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 40060a7fd93..6c38af642be 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.7.0", + "version": "2024.7.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", @@ -59,6 +59,7 @@ "clipboardRead", "clipboardWrite", "idle", + "alarms", "scripting", "offscreen", "webRequest", @@ -72,7 +73,12 @@ "sandbox": "sandbox allow-scripts; script-src 'self'" }, "sandbox": { - "pages": ["overlay/button.html", "overlay/list.html"] + "pages": [ + "overlay/menu-button.html", + "overlay/menu-list.html", + "overlay/button.html", + "overlay/list.html" + ] }, "commands": { "_execute_action": { @@ -112,6 +118,9 @@ "notification/bar.html", "images/icon38.png", "images/icon38_locked.png", + "overlay/menu-button.html", + "overlay/menu-list.html", + "overlay/menu.html", "overlay/button.html", "overlay/list.html", "popup/fonts/*" diff --git a/apps/browser/src/platform/alarms/alarm-state.ts b/apps/browser/src/platform/alarms/alarm-state.ts deleted file mode 100644 index fa18e26ed1c..00000000000 --- a/apps/browser/src/platform/alarms/alarm-state.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { clearClipboardAlarmName } from "../../autofill/clipboard"; -import { BrowserApi } from "../browser/browser-api"; - -export const alarmKeys = [clearClipboardAlarmName] as const; -export type AlarmKeys = (typeof alarmKeys)[number]; - -type AlarmState = { [T in AlarmKeys]: number | undefined }; - -const alarmState: AlarmState = { - clearClipboard: null, - //TODO once implemented vaultTimeout: null; - //TODO once implemented checkNotifications: null; - //TODO once implemented (if necessary) processReload: null; -}; - -/** - * Retrieves the set alarm time (planned execution) for a give an commandName {@link AlarmState} - * @param commandName A command that has been previously registered with {@link AlarmState} - * @returns {Promise} null or Unix epoch timestamp when the alarm action is supposed to execute - * @example - * // getAlarmTime(clearClipboard) - */ -export async function getAlarmTime(commandName: AlarmKeys): Promise { - let alarmTime: number; - if (BrowserApi.isManifestVersion(3)) { - const fromSessionStore = await chrome.storage.session.get(commandName); - alarmTime = fromSessionStore[commandName]; - } else { - alarmTime = alarmState[commandName]; - } - - return alarmTime; -} - -/** - * Registers an action that should execute after the given time has passed - * @param commandName A command that has been previously registered with {@link AlarmState} - * @param delay_ms The number of ms from now in which the command should execute from - * @example - * // setAlarmTime(clearClipboard, 5000) register the clearClipboard action which will execute when at least 5 seconds from now have passed - */ -export async function setAlarmTime(commandName: AlarmKeys, delay_ms: number): Promise { - if (!delay_ms || delay_ms === 0) { - await this.clearAlarmTime(commandName); - return; - } - - const time = Date.now() + delay_ms; - await setAlarmTimeInternal(commandName, time); -} - -/** - * Clears the time currently set for a given command - * @param commandName A command that has been previously registered with {@link AlarmState} - */ -export async function clearAlarmTime(commandName: AlarmKeys): Promise { - await setAlarmTimeInternal(commandName, null); -} - -async function setAlarmTimeInternal(commandName: AlarmKeys, time: number): Promise { - if (BrowserApi.isManifestVersion(3)) { - await chrome.storage.session.set({ [commandName]: time }); - } else { - alarmState[commandName] = time; - } -} diff --git a/apps/browser/src/platform/alarms/on-alarm-listener.ts b/apps/browser/src/platform/alarms/on-alarm-listener.ts deleted file mode 100644 index 274f19f7897..00000000000 --- a/apps/browser/src/platform/alarms/on-alarm-listener.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ClearClipboard, clearClipboardAlarmName } from "../../autofill/clipboard"; - -import { alarmKeys, clearAlarmTime, getAlarmTime } from "./alarm-state"; - -export const onAlarmListener = async (alarm: chrome.alarms.Alarm) => { - alarmKeys.forEach(async (key) => { - const executionTime = await getAlarmTime(key); - if (!executionTime) { - return; - } - - const currentDate = Date.now(); - if (executionTime > currentDate) { - return; - } - - await clearAlarmTime(key); - - switch (key) { - case clearClipboardAlarmName: - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - ClearClipboard.run(); - break; - default: - } - }); -}; diff --git a/apps/browser/src/platform/alarms/register-alarms.ts b/apps/browser/src/platform/alarms/register-alarms.ts deleted file mode 100644 index 86b9fb97747..00000000000 --- a/apps/browser/src/platform/alarms/register-alarms.ts +++ /dev/null @@ -1,31 +0,0 @@ -const NUMBER_OF_ALARMS = 6; - -export function registerAlarms() { - alarmsToBeCreated(NUMBER_OF_ALARMS); -} - -/** - * Creates staggered alarms that periodically (1min) raise OnAlarm events. The staggering is calculated based on the number of alarms passed in. - * @param numberOfAlarms Number of named alarms, that shall be registered - * @example - * // alarmsToBeCreated(2) results in 2 alarms separated by 30 seconds - * @example - * // alarmsToBeCreated(4) results in 4 alarms separated by 15 seconds - * @example - * // alarmsToBeCreated(6) results in 6 alarms separated by 10 seconds - * @example - * // alarmsToBeCreated(60) results in 60 alarms separated by 1 second - */ -function alarmsToBeCreated(numberOfAlarms: number): void { - const oneMinuteInMs = 60 * 1000; - const offset = oneMinuteInMs / numberOfAlarms; - - let calculatedWhen: number = Date.now() + offset; - - for (let index = 0; index < numberOfAlarms; index++) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - chrome.alarms.create(`bw_alarm${index}`, { periodInMinutes: 1, when: calculatedWhen }); - calculatedWhen += offset; - } -} diff --git a/apps/browser/src/platform/listeners/on-command-listener.ts b/apps/browser/src/platform/listeners/on-command-listener.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/browser/src/platform/popup/layout/popup-layout.mdx b/apps/browser/src/platform/popup/layout/popup-layout.mdx index 6f72f325bf1..b805805ad18 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.mdx +++ b/apps/browser/src/platform/popup/layout/popup-layout.mdx @@ -44,6 +44,14 @@ page looks nice when the extension is popped out. - default - Whatever content you want in `main`. +**Inputs** + +- `loading` + - When `true`, displays a loading state overlay instead of the default content. Defaults to + `false`. +- `loadingText` + - Custom text to be applied to the loading element for screenreaders only. Defaults to "Loading". + Basic usage example: ```html @@ -137,8 +145,20 @@ When the browser extension is popped out, the "popout" button should not be pass +# Other stories + ## Centered Content +An example of how to center the default content. + + +## Loading + +An example of what the loading state looks like. + + + + 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 9883a5cfb6f..f2208a8b8f5 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -62,27 +62,6 @@ class VaultComponent { protected data = Array.from(Array(20).keys()); } -@Component({ - selector: "generator-placeholder", - template: `
generator stuff here
`, - standalone: true, -}) -class GeneratorComponent {} - -@Component({ - selector: "send-placeholder", - template: `
send some stuff
`, - standalone: true, -}) -class SendComponent {} - -@Component({ - selector: "settings-placeholder", - template: `
change your settings
`, - standalone: true, -}) -class SettingsComponent {} - @Component({ selector: "mock-add-button", template: ` @@ -186,7 +165,7 @@ class MockVaultPagePoppedComponent {} - +
Generator content here
`, standalone: true, @@ -196,7 +175,6 @@ class MockVaultPagePoppedComponent {} MockAddButtonComponent, MockPopoutButtonComponent, MockCurrentAccountComponent, - GeneratorComponent, ], }) class MockGeneratorPageComponent {} @@ -212,7 +190,7 @@ class MockGeneratorPageComponent {} - +
Send content here
`, standalone: true, @@ -222,7 +200,6 @@ class MockGeneratorPageComponent {} MockAddButtonComponent, MockPopoutButtonComponent, MockCurrentAccountComponent, - SendComponent, ], }) class MockSendPageComponent {} @@ -238,7 +215,7 @@ class MockSendPageComponent {} - +
Settings content here
`, standalone: true, @@ -248,7 +225,6 @@ class MockSendPageComponent {} MockAddButtonComponent, MockPopoutButtonComponent, MockCurrentAccountComponent, - SettingsComponent, ], }) class MockSettingsPageComponent {} @@ -312,6 +288,7 @@ export default { useFactory: () => { return new I18nMockService({ back: "Back", + loading: "Loading", }); }, }, @@ -406,3 +383,19 @@ export const CenteredContent: Story = { `, }), }; + +export const Loading: Story = { + render: (args) => ({ + props: args, + template: /* HTML */ ` + + + + + Content would go here + + + + `, + }), +}; diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.html b/apps/browser/src/platform/popup/layout/popup-page.component.html index b3dcd626ae3..87f91e781a7 100644 --- a/apps/browser/src/platform/popup/layout/popup-page.component.html +++ b/apps/browser/src/platform/popup/layout/popup-page.component.html @@ -1,7 +1,16 @@ -
-
+
+
+ + +
diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.ts b/apps/browser/src/platform/popup/layout/popup-page.component.ts index 1223a6f4188..97a67fc852c 100644 --- a/apps/browser/src/platform/popup/layout/popup-page.component.ts +++ b/apps/browser/src/platform/popup/layout/popup-page.component.ts @@ -1,4 +1,7 @@ -import { Component } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { Component, Input, inject } from "@angular/core"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @Component({ selector: "popup-page", @@ -7,5 +10,13 @@ import { Component } from "@angular/core"; host: { class: "tw-h-full tw-flex tw-flex-col tw-flex-1 tw-overflow-y-auto", }, + imports: [CommonModule], }) -export class PopupPageComponent {} +export class PopupPageComponent { + protected i18nService = inject(I18nService); + + @Input() loading = false; + + /** Accessible loading label for the spinner. Defaults to "loading" */ + @Input() loadingText?: string = this.i18nService.t("loading"); +} diff --git a/apps/browser/src/platform/services/abstractions/browser-task-scheduler.service.ts b/apps/browser/src/platform/services/abstractions/browser-task-scheduler.service.ts new file mode 100644 index 00000000000..58c4eb48897 --- /dev/null +++ b/apps/browser/src/platform/services/abstractions/browser-task-scheduler.service.ts @@ -0,0 +1,33 @@ +import { Observable } from "rxjs"; + +import { TaskSchedulerService, ScheduledTaskName } from "@bitwarden/common/platform/scheduling"; + +export const BrowserTaskSchedulerPortName = "browser-task-scheduler-port"; + +export const BrowserTaskSchedulerPortActions = { + setTimeout: "setTimeout", + setInterval: "setInterval", + clearAlarm: "clearAlarm", +} as const; +export type BrowserTaskSchedulerPortAction = keyof typeof BrowserTaskSchedulerPortActions; + +export type BrowserTaskSchedulerPortMessage = { + action: BrowserTaskSchedulerPortAction; + taskName: ScheduledTaskName; + alarmName?: string; + delayInMs?: number; + intervalInMs?: number; +}; + +export type ActiveAlarm = { + alarmName: string; + startTime: number; + createInfo: chrome.alarms.AlarmCreateInfo; +}; + +export abstract class BrowserTaskSchedulerService extends TaskSchedulerService { + activeAlarms$: Observable; + abstract clearAllScheduledTasks(): Promise; + abstract verifyAlarmsState(): Promise; + abstract clearScheduledAlarm(alarmName: string): Promise; +} diff --git a/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.spec.ts b/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.spec.ts new file mode 100644 index 00000000000..ded57a5e85d --- /dev/null +++ b/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.spec.ts @@ -0,0 +1,129 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { Observable } from "rxjs"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; +import { GlobalState, StateProvider } from "@bitwarden/common/platform/state"; + +import { createPortSpyMock } from "../../../autofill/spec/autofill-mocks"; +import { + flushPromises, + sendPortMessage, + triggerPortOnDisconnectEvent, + triggerRuntimeOnConnectEvent, +} from "../../../autofill/spec/testing-utils"; +import { + BrowserTaskSchedulerPortActions, + BrowserTaskSchedulerPortName, +} from "../abstractions/browser-task-scheduler.service"; + +import { BackgroundTaskSchedulerService } from "./background-task-scheduler.service"; + +describe("BackgroundTaskSchedulerService", () => { + let logService: MockProxy; + let stateProvider: MockProxy; + let globalStateMock: MockProxy>; + let portMock: chrome.runtime.Port; + let backgroundTaskSchedulerService: BackgroundTaskSchedulerService; + + beforeEach(() => { + logService = mock(); + globalStateMock = mock>({ + state$: mock>(), + update: jest.fn((callback) => callback([], {} as any)), + }); + stateProvider = mock({ + getGlobal: jest.fn(() => globalStateMock), + }); + portMock = createPortSpyMock(BrowserTaskSchedulerPortName); + backgroundTaskSchedulerService = new BackgroundTaskSchedulerService(logService, stateProvider); + jest.spyOn(globalThis, "setTimeout"); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("ports on connect", () => { + it("ignores port connections that do not have the correct task scheduler port name", () => { + const portMockWithDifferentName = createPortSpyMock("different-name"); + triggerRuntimeOnConnectEvent(portMockWithDifferentName); + + expect(portMockWithDifferentName.onMessage.addListener).not.toHaveBeenCalled(); + expect(portMockWithDifferentName.onDisconnect.addListener).not.toHaveBeenCalled(); + }); + + it("sets up onMessage and onDisconnect listeners for connected ports", () => { + triggerRuntimeOnConnectEvent(portMock); + + expect(portMock.onMessage.addListener).toHaveBeenCalled(); + expect(portMock.onDisconnect.addListener).toHaveBeenCalled(); + }); + }); + + describe("ports on disconnect", () => { + it("removes the port from the set of connected ports", () => { + triggerRuntimeOnConnectEvent(portMock); + expect(backgroundTaskSchedulerService["ports"].size).toBe(1); + + triggerPortOnDisconnectEvent(portMock); + expect(backgroundTaskSchedulerService["ports"].size).toBe(0); + expect(portMock.onMessage.removeListener).toHaveBeenCalled(); + expect(portMock.onDisconnect.removeListener).toHaveBeenCalled(); + }); + }); + + describe("port message handlers", () => { + beforeEach(() => { + triggerRuntimeOnConnectEvent(portMock); + backgroundTaskSchedulerService.registerTaskHandler( + ScheduledTaskNames.loginStrategySessionTimeout, + jest.fn(), + ); + }); + + it("sets a setTimeout backup alarm", async () => { + sendPortMessage(portMock, { + action: BrowserTaskSchedulerPortActions.setTimeout, + taskName: ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs: 1000, + }); + await flushPromises(); + + expect(globalThis.setTimeout).toHaveBeenCalled(); + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { delayInMinutes: 0.5 }, + expect.any(Function), + ); + }); + + it("sets a setInterval backup alarm", async () => { + sendPortMessage(portMock, { + action: BrowserTaskSchedulerPortActions.setInterval, + taskName: ScheduledTaskNames.loginStrategySessionTimeout, + intervalInMs: 600000, + }); + await flushPromises(); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { delayInMinutes: 10, periodInMinutes: 10 }, + expect.any(Function), + ); + }); + + it("clears a scheduled alarm", async () => { + sendPortMessage(portMock, { + action: BrowserTaskSchedulerPortActions.clearAlarm, + alarmName: ScheduledTaskNames.loginStrategySessionTimeout, + }); + await flushPromises(); + + expect(chrome.alarms.clear).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + expect.any(Function), + ); + }); + }); +}); diff --git a/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.ts b/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.ts new file mode 100644 index 00000000000..23b580988f8 --- /dev/null +++ b/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.ts @@ -0,0 +1,75 @@ +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { StateProvider } from "@bitwarden/common/platform/state"; + +import { BrowserApi } from "../../browser/browser-api"; +import { + BrowserTaskSchedulerPortActions, + BrowserTaskSchedulerPortMessage, + BrowserTaskSchedulerPortName, +} from "../abstractions/browser-task-scheduler.service"; + +import { BrowserTaskSchedulerServiceImplementation } from "./browser-task-scheduler.service"; + +export class BackgroundTaskSchedulerService extends BrowserTaskSchedulerServiceImplementation { + private ports: Set = new Set(); + + constructor(logService: LogService, stateProvider: StateProvider) { + super(logService, stateProvider); + + BrowserApi.addListener(chrome.runtime.onConnect, this.handlePortOnConnect); + } + + /** + * Handles a port connection made from the foreground task scheduler. + * + * @param port - The port that was connected. + */ + private handlePortOnConnect = (port: chrome.runtime.Port) => { + if (port.name !== BrowserTaskSchedulerPortName) { + return; + } + + this.ports.add(port); + port.onMessage.addListener(this.handlePortMessage); + port.onDisconnect.addListener(this.handlePortOnDisconnect); + }; + + /** + * Handles a port disconnection. + * + * @param port - The port that was disconnected. + */ + private handlePortOnDisconnect = (port: chrome.runtime.Port) => { + port.onMessage.removeListener(this.handlePortMessage); + port.onDisconnect.removeListener(this.handlePortOnDisconnect); + this.ports.delete(port); + }; + + /** + * Handles a message from a port. + * + * @param message - The message that was received. + * @param port - The port that sent the message. + */ + private handlePortMessage = ( + message: BrowserTaskSchedulerPortMessage, + port: chrome.runtime.Port, + ) => { + const isTaskSchedulerPort = port.name === BrowserTaskSchedulerPortName; + const { action, taskName, alarmName, delayInMs, intervalInMs } = message; + + if (isTaskSchedulerPort && action === BrowserTaskSchedulerPortActions.setTimeout) { + super.setTimeout(taskName, delayInMs); + return; + } + + if (isTaskSchedulerPort && action === BrowserTaskSchedulerPortActions.setInterval) { + super.setInterval(taskName, intervalInMs); + return; + } + + if (isTaskSchedulerPort && action === BrowserTaskSchedulerPortActions.clearAlarm) { + super.clearScheduledAlarm(alarmName).catch((error) => this.logService.error(error)); + } + }; +} diff --git a/apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.spec.ts b/apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.spec.ts new file mode 100644 index 00000000000..d72ba942051 --- /dev/null +++ b/apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.spec.ts @@ -0,0 +1,463 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject, Observable } from "rxjs"; + +import { ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; +import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; +import { GlobalState, StateProvider } from "@bitwarden/common/platform/state"; + +import { flushPromises, triggerOnAlarmEvent } from "../../../autofill/spec/testing-utils"; +import { + ActiveAlarm, + BrowserTaskSchedulerService, +} from "../abstractions/browser-task-scheduler.service"; + +import { BrowserTaskSchedulerServiceImplementation } from "./browser-task-scheduler.service"; + +jest.mock("rxjs", () => { + const actualModule = jest.requireActual("rxjs"); + return { + ...actualModule, + firstValueFrom: jest.fn((state$: BehaviorSubject) => state$.value), + }; +}); + +function setupGlobalBrowserMock(overrides: Partial = {}) { + globalThis.browser.alarms = { + create: jest.fn(), + clear: jest.fn(), + get: jest.fn(), + getAll: jest.fn(), + clearAll: jest.fn(), + onAlarm: { + addListener: jest.fn(), + removeListener: jest.fn(), + hasListener: jest.fn(), + }, + ...overrides, + }; +} + +describe("BrowserTaskSchedulerService", () => { + const callback = jest.fn(); + const delayInMinutes = 2; + let activeAlarmsMock$: BehaviorSubject; + let logService: MockProxy; + let stateProvider: MockProxy; + let globalStateMock: MockProxy>; + let browserTaskSchedulerService: BrowserTaskSchedulerService; + let activeAlarms: ActiveAlarm[] = []; + const eventUploadsIntervalCreateInfo = { periodInMinutes: 5, delayInMinutes: 5 }; + const scheduleNextSyncIntervalCreateInfo = { periodInMinutes: 5, delayInMinutes: 5 }; + + beforeEach(() => { + jest.useFakeTimers(); + activeAlarms = [ + mock({ + alarmName: ScheduledTaskNames.eventUploadsInterval, + createInfo: eventUploadsIntervalCreateInfo, + }), + mock({ + alarmName: ScheduledTaskNames.scheduleNextSyncInterval, + createInfo: scheduleNextSyncIntervalCreateInfo, + }), + mock({ + alarmName: ScheduledTaskNames.fido2ClientAbortTimeout, + startTime: Date.now() - 60001, + createInfo: { delayInMinutes: 1, periodInMinutes: undefined }, + }), + ]; + activeAlarmsMock$ = new BehaviorSubject(activeAlarms); + logService = mock(); + globalStateMock = mock>({ + state$: mock>(), + update: jest.fn((callback) => callback([], {} as any)), + }); + stateProvider = mock({ + getGlobal: jest.fn(() => globalStateMock), + }); + browserTaskSchedulerService = new BrowserTaskSchedulerServiceImplementation( + logService, + stateProvider, + ); + browserTaskSchedulerService.activeAlarms$ = activeAlarmsMock$; + browserTaskSchedulerService.registerTaskHandler( + ScheduledTaskNames.loginStrategySessionTimeout, + callback, + ); + // @ts-expect-error mocking global browser object + // eslint-disable-next-line no-global-assign + globalThis.browser = {}; + chrome.alarms.get = jest.fn().mockImplementation((_name, callback) => callback(undefined)); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.clearAllTimers(); + jest.useRealTimers(); + + // eslint-disable-next-line no-global-assign + globalThis.browser = undefined; + }); + + describe("setTimeout", () => { + it("triggers an error when setting a timeout for a task that is not registered", async () => { + expect(() => + browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.notificationsReconnectTimeout, + 1000, + ), + ).toThrow( + `Task handler for ${ScheduledTaskNames.notificationsReconnectTimeout} not registered. Unable to schedule task.`, + ); + }); + + it("creates a timeout alarm", async () => { + browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMinutes * 60 * 1000, + ); + await flushPromises(); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { delayInMinutes }, + expect.any(Function), + ); + }); + + it("skips creating a duplicate timeout alarm", async () => { + const mockAlarm = mock(); + chrome.alarms.get = jest.fn().mockImplementation((_name, callback) => callback(mockAlarm)); + + browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMinutes * 60 * 1000, + ); + + expect(chrome.alarms.create).not.toHaveBeenCalled(); + }); + + describe("when the task is scheduled to be triggered in less than the minimum possible delay", () => { + const delayInMs = 25000; + + it("sets a timeout using the global setTimeout API", async () => { + jest.spyOn(globalThis, "setTimeout"); + + browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs, + ); + await flushPromises(); + + expect(globalThis.setTimeout).toHaveBeenCalledWith(expect.any(Function), delayInMs); + }); + + it("sets a fallback alarm", async () => { + const delayInMs = 15000; + browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs, + ); + await flushPromises(); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { delayInMinutes: 0.5 }, + expect.any(Function), + ); + }); + + it("sets the fallback for a minimum of 1 minute if the environment not for Chrome", async () => { + setupGlobalBrowserMock(); + + browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs, + ); + await flushPromises(); + + expect(browser.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { delayInMinutes: 1 }, + ); + }); + + it("clears the fallback alarm when the setTimeout is triggered", async () => { + jest.useFakeTimers(); + + browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs, + ); + jest.advanceTimersByTime(delayInMs); + await flushPromises(); + + expect(chrome.alarms.clear).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + expect.any(Function), + ); + }); + }); + + it("returns a subscription that can be used to clear the timeout", () => { + jest.spyOn(globalThis, "clearTimeout"); + + const timeoutSubscription = browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + 10000, + ); + + timeoutSubscription.unsubscribe(); + + expect(chrome.alarms.clear).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + expect.any(Function), + ); + expect(globalThis.clearTimeout).toHaveBeenCalled(); + }); + + it("clears alarms in non-chrome environments", () => { + setupGlobalBrowserMock(); + + const timeoutSubscription = browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + 10000, + ); + timeoutSubscription.unsubscribe(); + + expect(browser.alarms.clear).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + ); + }); + }); + + describe("setInterval", () => { + it("triggers an error when setting an interval for a task that is not registered", async () => { + expect(() => { + browserTaskSchedulerService.setInterval( + ScheduledTaskNames.notificationsReconnectTimeout, + 1000, + ); + }).toThrow( + `Task handler for ${ScheduledTaskNames.notificationsReconnectTimeout} not registered. Unable to schedule task.`, + ); + }); + + describe("setting an interval that is less than 1 minute", () => { + const intervalInMs = 10000; + + it("sets up stepped alarms that trigger behavior after the first minute of setInterval execution", async () => { + browserTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + intervalInMs, + ); + await flushPromises(); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__0`, + { periodInMinutes: 0.6666666666666666, delayInMinutes: 0.5 }, + expect.any(Function), + ); + expect(chrome.alarms.create).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__1`, + { periodInMinutes: 0.6666666666666666, delayInMinutes: 0.6666666666666666 }, + expect.any(Function), + ); + expect(chrome.alarms.create).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__2`, + { periodInMinutes: 0.6666666666666666, delayInMinutes: 0.8333333333333333 }, + expect.any(Function), + ); + expect(chrome.alarms.create).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__3`, + { periodInMinutes: 0.6666666666666666, delayInMinutes: 1 }, + expect.any(Function), + ); + }); + + it("sets an interval using the global setInterval API", async () => { + jest.spyOn(globalThis, "setInterval"); + + browserTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + intervalInMs, + ); + await flushPromises(); + + expect(globalThis.setInterval).toHaveBeenCalledWith(expect.any(Function), intervalInMs); + }); + + it("clears the global setInterval instance once the interval has elapsed the minimum required delay for an alarm", async () => { + jest.useFakeTimers(); + jest.spyOn(globalThis, "clearInterval"); + + browserTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + intervalInMs, + ); + await flushPromises(); + jest.advanceTimersByTime(50000); + + expect(globalThis.clearInterval).toHaveBeenCalledWith(expect.any(Number)); + }); + }); + + it("creates an interval alarm", async () => { + const periodInMinutes = 2; + const initialDelayInMs = 1000; + + browserTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + periodInMinutes * 60 * 1000, + initialDelayInMs, + ); + await flushPromises(); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { periodInMinutes, delayInMinutes: 0.5 }, + expect.any(Function), + ); + }); + + it("defaults the alarm's delay in minutes to the interval in minutes if the delay is not specified", async () => { + const periodInMinutes = 2; + browserTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + periodInMinutes * 60 * 1000, + ); + await flushPromises(); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { periodInMinutes, delayInMinutes: periodInMinutes }, + expect.any(Function), + ); + }); + + it("returns a subscription that can be used to clear an interval alarm", () => { + jest.spyOn(globalThis, "clearInterval"); + + const intervalSubscription = browserTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + 600000, + ); + + intervalSubscription.unsubscribe(); + + expect(chrome.alarms.clear).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + expect.any(Function), + ); + expect(globalThis.clearInterval).not.toHaveBeenCalled(); + }); + + it("returns a subscription that can be used to clear all stepped interval alarms", () => { + jest.spyOn(globalThis, "clearInterval"); + + const intervalSubscription = browserTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + 10000, + ); + + intervalSubscription.unsubscribe(); + + expect(chrome.alarms.clear).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__0`, + expect.any(Function), + ); + expect(chrome.alarms.clear).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__1`, + expect.any(Function), + ); + expect(chrome.alarms.clear).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__2`, + expect.any(Function), + ); + expect(chrome.alarms.clear).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__3`, + expect.any(Function), + ); + expect(globalThis.clearInterval).toHaveBeenCalled(); + }); + }); + + describe("verifyAlarmsState", () => { + it("skips recovering a scheduled task if an existing alarm for the task is present", async () => { + chrome.alarms.get = jest + .fn() + .mockImplementation((_name, callback) => callback(mock())); + + await browserTaskSchedulerService.verifyAlarmsState(); + + expect(chrome.alarms.create).not.toHaveBeenCalled(); + expect(callback).not.toHaveBeenCalled(); + }); + + describe("extension alarm is not set", () => { + it("triggers the task when the task should have triggered", async () => { + const fido2Callback = jest.fn(); + browserTaskSchedulerService.registerTaskHandler( + ScheduledTaskNames.fido2ClientAbortTimeout, + fido2Callback, + ); + + await browserTaskSchedulerService.verifyAlarmsState(); + + expect(fido2Callback).toHaveBeenCalled(); + }); + + it("schedules an alarm for the task when it has not yet triggered ", async () => { + const syncCallback = jest.fn(); + browserTaskSchedulerService.registerTaskHandler( + ScheduledTaskNames.scheduleNextSyncInterval, + syncCallback, + ); + + await browserTaskSchedulerService.verifyAlarmsState(); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.scheduleNextSyncInterval, + scheduleNextSyncIntervalCreateInfo, + expect.any(Function), + ); + }); + }); + }); + + describe("triggering a task", () => { + it("triggers a task when an onAlarm event is triggered", () => { + const alarm = mock({ + name: ScheduledTaskNames.loginStrategySessionTimeout, + }); + + triggerOnAlarmEvent(alarm); + + expect(callback).toHaveBeenCalled(); + }); + }); + + describe("clearAllScheduledTasks", () => { + it("clears all scheduled tasks and extension alarms", async () => { + // @ts-expect-error mocking global state update method + globalStateMock.update = jest.fn((callback) => { + const stateValue = callback([], {} as any); + activeAlarmsMock$.next(stateValue); + return stateValue; + }); + + await browserTaskSchedulerService.clearAllScheduledTasks(); + + expect(chrome.alarms.clearAll).toHaveBeenCalled(); + expect(activeAlarmsMock$.value).toEqual([]); + }); + + it("clears all extension alarms within a non Chrome environment", async () => { + setupGlobalBrowserMock(); + + await browserTaskSchedulerService.clearAllScheduledTasks(); + + expect(browser.alarms.clearAll).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.ts b/apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.ts new file mode 100644 index 00000000000..187742f5891 --- /dev/null +++ b/apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.ts @@ -0,0 +1,427 @@ +import { firstValueFrom, map, Observable, Subscription } from "rxjs"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { + DefaultTaskSchedulerService, + ScheduledTaskName, +} from "@bitwarden/common/platform/scheduling"; +import { + TASK_SCHEDULER_DISK, + GlobalState, + KeyDefinition, + StateProvider, +} from "@bitwarden/common/platform/state"; + +import { BrowserApi } from "../../browser/browser-api"; +import { + ActiveAlarm, + BrowserTaskSchedulerService, +} from "../abstractions/browser-task-scheduler.service"; + +const ACTIVE_ALARMS = new KeyDefinition(TASK_SCHEDULER_DISK, "activeAlarms", { + deserializer: (value: ActiveAlarm[]) => value ?? [], +}); + +export class BrowserTaskSchedulerServiceImplementation + extends DefaultTaskSchedulerService + implements BrowserTaskSchedulerService +{ + private activeAlarmsState: GlobalState; + readonly activeAlarms$: Observable; + + constructor( + logService: LogService, + private stateProvider: StateProvider, + ) { + super(logService); + + this.activeAlarmsState = this.stateProvider.getGlobal(ACTIVE_ALARMS); + this.activeAlarms$ = this.activeAlarmsState.state$.pipe( + map((activeAlarms) => activeAlarms ?? []), + ); + + this.setupOnAlarmListener(); + } + + /** + * Sets a timeout to execute a callback after a delay. If the delay is less + * than 1 minute, it will use the global setTimeout. Otherwise, it will + * create a browser extension alarm to handle the delay. + * + * @param taskName - The name of the task, used in defining the alarm. + * @param delayInMs - The delay in milliseconds. + */ + setTimeout(taskName: ScheduledTaskName, delayInMs: number): Subscription { + let timeoutHandle: number | NodeJS.Timeout; + this.validateRegisteredTask(taskName); + + const delayInMinutes = delayInMs / 1000 / 60; + this.scheduleAlarm(taskName, { + delayInMinutes: this.getUpperBoundDelayInMinutes(delayInMinutes), + }).catch((error) => this.logService.error("Failed to schedule alarm", error)); + + // If the delay is less than a minute, we want to attempt to trigger the task through a setTimeout. + // The alarm previously scheduled will be used as a backup in case the setTimeout fails. + if (delayInMinutes < this.getUpperBoundDelayInMinutes(delayInMinutes)) { + timeoutHandle = globalThis.setTimeout(async () => { + await this.clearScheduledAlarm(taskName); + await this.triggerTask(taskName); + }, delayInMs); + } + + return new Subscription(() => { + if (timeoutHandle) { + globalThis.clearTimeout(timeoutHandle); + } + this.clearScheduledAlarm(taskName).catch((error) => + this.logService.error("Failed to clear alarm", error), + ); + }); + } + + /** + * Sets an interval to execute a callback at each interval. If the interval is + * less than 1 minute, it will use the global setInterval. Otherwise, it will + * create a browser extension alarm to handle the interval. + * + * @param taskName - The name of the task, used in defining the alarm. + * @param intervalInMs - The interval in milliseconds. + * @param initialDelayInMs - The initial delay in milliseconds. + */ + setInterval( + taskName: ScheduledTaskName, + intervalInMs: number, + initialDelayInMs?: number, + ): Subscription { + this.validateRegisteredTask(taskName); + + const intervalInMinutes = intervalInMs / 1000 / 60; + const initialDelayInMinutes = initialDelayInMs + ? initialDelayInMs / 1000 / 60 + : intervalInMinutes; + + if (intervalInMinutes < this.getUpperBoundDelayInMinutes(intervalInMinutes)) { + return this.setupSteppedIntervalAlarms(taskName, intervalInMs); + } + + this.scheduleAlarm(taskName, { + periodInMinutes: this.getUpperBoundDelayInMinutes(intervalInMinutes), + delayInMinutes: this.getUpperBoundDelayInMinutes(initialDelayInMinutes), + }).catch((error) => this.logService.error("Failed to schedule alarm", error)); + + return new Subscription(() => + this.clearScheduledAlarm(taskName).catch((error) => + this.logService.error("Failed to clear alarm", error), + ), + ); + } + + /** + * Used in cases where the interval is less than 1 minute. This method will set up a setInterval + * to initialize expected recurring behavior, then create a series of alarms to handle the + * expected scheduled task through the alarms api. This is necessary because the alarms + * api does not support intervals less than 1 minute. + * + * @param taskName - The name of the task + * @param intervalInMs - The interval in milliseconds. + */ + private setupSteppedIntervalAlarms( + taskName: ScheduledTaskName, + intervalInMs: number, + ): Subscription { + const alarmMinDelayInMinutes = this.getAlarmMinDelayInMinutes(); + const intervalInMinutes = intervalInMs / 1000 / 60; + const numberOfAlarmsToCreate = Math.ceil(Math.ceil(1 / intervalInMinutes) / 2) + 1; + const steppedAlarmPeriodInMinutes = alarmMinDelayInMinutes + intervalInMinutes; + const steppedAlarmNames: string[] = []; + for (let alarmIndex = 0; alarmIndex < numberOfAlarmsToCreate; alarmIndex++) { + const steppedAlarmName = `${taskName}__${alarmIndex}`; + steppedAlarmNames.push(steppedAlarmName); + + const delayInMinutes = this.getUpperBoundDelayInMinutes( + alarmMinDelayInMinutes + intervalInMinutes * alarmIndex, + ); + + this.clearScheduledAlarm(steppedAlarmName) + .then(() => + this.scheduleAlarm(steppedAlarmName, { + periodInMinutes: steppedAlarmPeriodInMinutes, + delayInMinutes, + }).catch((error) => this.logService.error("Failed to schedule alarm", error)), + ) + .catch((error) => this.logService.error("Failed to clear alarm", error)); + } + + let elapsedMs = 0; + const intervalHandle: number | NodeJS.Timeout = globalThis.setInterval(async () => { + elapsedMs += intervalInMs; + const elapsedMinutes = elapsedMs / 1000 / 60; + + if (elapsedMinutes >= alarmMinDelayInMinutes) { + globalThis.clearInterval(intervalHandle); + return; + } + + await this.triggerTask(taskName, intervalInMinutes); + }, intervalInMs); + + return new Subscription(() => { + if (intervalHandle) { + globalThis.clearInterval(intervalHandle); + } + steppedAlarmNames.forEach((alarmName) => + this.clearScheduledAlarm(alarmName).catch((error) => + this.logService.error("Failed to clear alarm", error), + ), + ); + }); + } + + /** + * Clears all scheduled tasks by clearing all browser extension + * alarms and resetting the active alarms state. + */ + async clearAllScheduledTasks(): Promise { + await this.clearAllAlarms(); + await this.updateActiveAlarms([]); + } + + /** + * Verifies the state of the active alarms by checking if + * any alarms have been missed or need to be created. + */ + async verifyAlarmsState(): Promise { + const currentTime = Date.now(); + const activeAlarms = await this.getActiveAlarms(); + + for (const alarm of activeAlarms) { + const { alarmName, startTime, createInfo } = alarm; + const existingAlarm = await this.getAlarm(alarmName); + if (existingAlarm) { + continue; + } + + const shouldAlarmHaveBeenTriggered = createInfo.when && createInfo.when < currentTime; + const hasSetTimeoutAlarmExceededDelay = + !createInfo.periodInMinutes && + createInfo.delayInMinutes && + startTime + createInfo.delayInMinutes * 60 * 1000 < currentTime; + if (shouldAlarmHaveBeenTriggered || hasSetTimeoutAlarmExceededDelay) { + await this.triggerTask(alarmName); + continue; + } + + this.scheduleAlarm(alarmName, createInfo).catch((error) => + this.logService.error("Failed to schedule alarm", error), + ); + } + } + + /** + * Creates a browser extension alarm with the given name and create info. + * + * @param alarmName - The name of the alarm. + * @param createInfo - The alarm create info. + */ + private async scheduleAlarm( + alarmName: string, + createInfo: chrome.alarms.AlarmCreateInfo, + ): Promise { + const existingAlarm = await this.getAlarm(alarmName); + if (existingAlarm) { + this.logService.debug(`Alarm ${alarmName} already exists. Skipping creation.`); + return; + } + + await this.createAlarm(alarmName, createInfo); + await this.setActiveAlarm(alarmName, createInfo); + } + + /** + * Gets the active alarms from state. + */ + private async getActiveAlarms(): Promise { + return await firstValueFrom(this.activeAlarms$); + } + + /** + * Sets an active alarm in state. + * + * @param alarmName - The name of the active alarm to set. + * @param createInfo - The creation info of the active alarm. + */ + private async setActiveAlarm( + alarmName: string, + createInfo: chrome.alarms.AlarmCreateInfo, + ): Promise { + const activeAlarms = await this.getActiveAlarms(); + const filteredAlarms = activeAlarms.filter((alarm) => alarm.alarmName !== alarmName); + filteredAlarms.push({ + alarmName, + startTime: Date.now(), + createInfo, + }); + await this.updateActiveAlarms(filteredAlarms); + } + + /** + * Deletes an active alarm from state. + * + * @param alarmName - The name of the active alarm to delete. + */ + private async deleteActiveAlarm(alarmName: string): Promise { + const activeAlarms = await this.getActiveAlarms(); + const filteredAlarms = activeAlarms.filter((alarm) => alarm.alarmName !== alarmName); + await this.updateActiveAlarms(filteredAlarms || []); + } + + /** + * Clears a scheduled alarm by its name and deletes it from the active alarms state. + * + * @param alarmName - The name of the alarm to clear. + */ + async clearScheduledAlarm(alarmName: string): Promise { + const wasCleared = await this.clearAlarm(alarmName); + if (wasCleared) { + await this.deleteActiveAlarm(alarmName); + } + } + + /** + * Updates the active alarms state with the given alarms. + * + * @param alarms - The alarms to update the state with. + */ + private async updateActiveAlarms(alarms: ActiveAlarm[]): Promise { + await this.activeAlarmsState.update(() => alarms); + } + + /** + * Sets up the on alarm listener to handle alarms. + */ + private setupOnAlarmListener(): void { + BrowserApi.addListener(chrome.alarms.onAlarm, this.handleOnAlarm); + } + + /** + * Handles on alarm events, triggering the alarm if a handler exists. + * + * @param alarm - The alarm to handle. + */ + private handleOnAlarm = async (alarm: chrome.alarms.Alarm): Promise => { + const { name, periodInMinutes } = alarm; + await this.triggerTask(name, periodInMinutes); + }; + + /** + * Triggers an alarm by calling its handler and + * deleting it if it is a one-time alarm. + * + * @param alarmName - The name of the alarm to trigger. + * @param periodInMinutes - The period in minutes of an interval alarm. + */ + protected async triggerTask(alarmName: string, periodInMinutes?: number): Promise { + const taskName = this.getTaskFromAlarmName(alarmName); + const handler = this.taskHandlers.get(taskName); + if (!periodInMinutes) { + await this.deleteActiveAlarm(alarmName); + } + + if (handler) { + handler(); + } + } + + /** + * Parses and returns the task name from an alarm name. + * + * @param alarmName - The alarm name to parse. + */ + protected getTaskFromAlarmName(alarmName: string): ScheduledTaskName { + return alarmName.split("__")[0] as ScheduledTaskName; + } + + /** + * Clears a new alarm with the given name and create info. Returns a promise + * that indicates when the alarm has been cleared successfully. + * + * @param alarmName - The name of the alarm to create. + */ + private async clearAlarm(alarmName: string): Promise { + if (this.isNonChromeEnvironment()) { + return browser.alarms.clear(alarmName); + } + + return new Promise((resolve) => chrome.alarms.clear(alarmName, resolve)); + } + + /** + * Clears all alarms that have been set by the extension. Returns a promise + * that indicates when all alarms have been cleared successfully. + */ + private clearAllAlarms(): Promise { + if (this.isNonChromeEnvironment()) { + return browser.alarms.clearAll(); + } + + return new Promise((resolve) => chrome.alarms.clearAll(resolve)); + } + + /** + * Creates a new alarm with the given name and create info. + * + * @param alarmName - The name of the alarm to create. + * @param createInfo - The creation info for the alarm. + */ + private async createAlarm( + alarmName: string, + createInfo: chrome.alarms.AlarmCreateInfo, + ): Promise { + if (this.isNonChromeEnvironment()) { + return browser.alarms.create(alarmName, createInfo); + } + + return new Promise((resolve) => chrome.alarms.create(alarmName, createInfo, resolve)); + } + + /** + * Gets the alarm with the given name. + * + * @param alarmName - The name of the alarm to get. + */ + private getAlarm(alarmName: string): Promise { + if (this.isNonChromeEnvironment()) { + return browser.alarms.get(alarmName); + } + + return new Promise((resolve) => chrome.alarms.get(alarmName, resolve)); + } + + /** + * Checks if the environment is a non-Chrome environment. This is used to determine + * if the browser alarms API should be used in place of the chrome alarms API. This + * is necessary because the `chrome` polyfill that Mozilla implements does not allow + * passing the callback parameter in the same way most `chrome.alarm` api calls allow. + */ + private isNonChromeEnvironment(): boolean { + return typeof browser !== "undefined" && !!browser.alarms; + } + + /** + * Gets the minimum delay in minutes for an alarm. This is used to ensure that the + * delay is at least 1 minute in non-Chrome environments. In Chrome environments, the + * delay can be as low as 0.5 minutes. + */ + private getAlarmMinDelayInMinutes(): number { + return this.isNonChromeEnvironment() ? 1 : 0.5; + } + + /** + * Gets the upper bound delay in minutes for a given delay in minutes. + * + * @param delayInMinutes - The delay in minutes. + */ + private getUpperBoundDelayInMinutes(delayInMinutes: number): number { + return Math.max(this.getAlarmMinDelayInMinutes(), delayInMinutes); + } +} diff --git a/apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.spec.ts b/apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.spec.ts new file mode 100644 index 00000000000..e0ee49c5fa1 --- /dev/null +++ b/apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.spec.ts @@ -0,0 +1,79 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { Observable } from "rxjs"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; +import { GlobalState, StateProvider } from "@bitwarden/common/platform/state"; + +import { createPortSpyMock } from "../../../autofill/spec/autofill-mocks"; +import { flushPromises } from "../../../autofill/spec/testing-utils"; +import { + BrowserTaskSchedulerPortActions, + BrowserTaskSchedulerPortName, +} from "../abstractions/browser-task-scheduler.service"; + +import { ForegroundTaskSchedulerService } from "./foreground-task-scheduler.service"; + +describe("ForegroundTaskSchedulerService", () => { + let logService: MockProxy; + let stateProvider: MockProxy; + let globalStateMock: MockProxy>; + let portMock: chrome.runtime.Port; + let foregroundTaskSchedulerService: ForegroundTaskSchedulerService; + + beforeEach(() => { + logService = mock(); + globalStateMock = mock>({ + state$: mock>(), + update: jest.fn((callback) => callback([], {} as any)), + }); + stateProvider = mock({ + getGlobal: jest.fn(() => globalStateMock), + }); + portMock = createPortSpyMock(BrowserTaskSchedulerPortName); + foregroundTaskSchedulerService = new ForegroundTaskSchedulerService(logService, stateProvider); + foregroundTaskSchedulerService["port"] = portMock; + foregroundTaskSchedulerService.registerTaskHandler( + ScheduledTaskNames.loginStrategySessionTimeout, + jest.fn(), + ); + jest.spyOn(globalThis, "setTimeout"); + jest.spyOn(globalThis, "setInterval"); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("sets a timeout for a task and sends a message to the background to set up a backup timeout alarm", async () => { + foregroundTaskSchedulerService.setTimeout(ScheduledTaskNames.loginStrategySessionTimeout, 1000); + await flushPromises(); + + expect(globalThis.setTimeout).toHaveBeenCalledWith(expect.any(Function), 1000); + expect(chrome.alarms.create).toHaveBeenCalledWith( + "loginStrategySessionTimeout", + { delayInMinutes: 0.5 }, + expect.any(Function), + ); + expect(portMock.postMessage).toHaveBeenCalledWith({ + action: BrowserTaskSchedulerPortActions.setTimeout, + taskName: ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs: 1000, + }); + }); + + it("sets an interval for a task and sends a message to the background to set up a backup interval alarm", async () => { + foregroundTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + 1000, + ); + await flushPromises(); + + expect(globalThis.setInterval).toHaveBeenCalledWith(expect.any(Function), 1000); + expect(portMock.postMessage).toHaveBeenCalledWith({ + action: BrowserTaskSchedulerPortActions.setInterval, + taskName: ScheduledTaskNames.loginStrategySessionTimeout, + intervalInMs: 1000, + }); + }); +}); diff --git a/apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.ts b/apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.ts new file mode 100644 index 00000000000..af4d56aa62a --- /dev/null +++ b/apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.ts @@ -0,0 +1,71 @@ +import { Subscription } from "rxjs"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { ScheduledTaskName } from "@bitwarden/common/platform/scheduling"; +import { StateProvider } from "@bitwarden/common/platform/state"; + +import { + BrowserTaskSchedulerPortActions, + BrowserTaskSchedulerPortMessage, + BrowserTaskSchedulerPortName, +} from "../abstractions/browser-task-scheduler.service"; + +import { BrowserTaskSchedulerServiceImplementation } from "./browser-task-scheduler.service"; + +export class ForegroundTaskSchedulerService extends BrowserTaskSchedulerServiceImplementation { + private port: chrome.runtime.Port; + + constructor(logService: LogService, stateProvider: StateProvider) { + super(logService, stateProvider); + + this.port = chrome.runtime.connect({ name: BrowserTaskSchedulerPortName }); + } + + /** + * Sends a port message to the background to set up a fallback timeout. Also sets a timeout locally. + * This is done to ensure that the timeout triggers even if the popup is closed. + * + * @param taskName - The name of the task. + * @param delayInMs - The delay in milliseconds. + */ + setTimeout(taskName: ScheduledTaskName, delayInMs: number): Subscription { + this.sendPortMessage({ + action: BrowserTaskSchedulerPortActions.setTimeout, + taskName, + delayInMs, + }); + + return super.setTimeout(taskName, delayInMs); + } + + /** + * Sends a port message to the background to set up a fallback interval. Also sets an interval locally. + * This is done to ensure that the interval triggers even if the popup is closed. + * + * @param taskName - The name of the task. + * @param intervalInMs - The interval in milliseconds. + * @param initialDelayInMs - The initial delay in milliseconds. + */ + setInterval( + taskName: ScheduledTaskName, + intervalInMs: number, + initialDelayInMs?: number, + ): Subscription { + this.sendPortMessage({ + action: BrowserTaskSchedulerPortActions.setInterval, + taskName, + intervalInMs, + }); + + return super.setInterval(taskName, intervalInMs, initialDelayInMs); + } + + /** + * Sends a message to the background task scheduler. + * + * @param message - The message to send. + */ + private sendPortMessage(message: BrowserTaskSchedulerPortMessage) { + this.port.postMessage(message); + } +} diff --git a/apps/browser/src/popup/app-routing.animations.ts b/apps/browser/src/popup/app-routing.animations.ts index 065331bd414..227ede146ba 100644 --- a/apps/browser/src/popup/app-routing.animations.ts +++ b/apps/browser/src/popup/app-routing.animations.ts @@ -177,6 +177,9 @@ export const routerTransition = trigger("routerTransition", [ transition("tabs => account-security", inSlideLeft), transition("account-security => tabs", outSlideRight), + transition("tabs => assign-collections", inSlideLeft), + transition("assign-collections => tabs", outSlideRight), + // Vault settings transition("tabs => vault-settings", inSlideLeft), transition("vault-settings => tabs", outSlideRight), diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 8645cb797bd..53ab778c31d 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -48,6 +48,7 @@ import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/pass import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.component"; import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component"; import { SendTypeComponent } from "../tools/popup/send/send-type.component"; +import { SendV2Component } from "../tools/popup/send/send-v2.component"; import { AboutPageV2Component } from "../tools/popup/settings/about-page/about-page-v2.component"; import { AboutPageComponent } from "../tools/popup/settings/about-page/about-page.component"; import { MoreFromBitwardenPageV2Component } from "../tools/popup/settings/about-page/more-from-bitwarden-page-v2.component"; @@ -70,6 +71,7 @@ import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component"; import { ViewComponent } from "../vault/popup/components/vault/view.component"; import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/add-edit-v2.component"; +import { AssignCollections } from "../vault/popup/components/vault-v2/assign-collections/assign-collections.component"; import { AttachmentsV2Component } from "../vault/popup/components/vault-v2/attachments/attachments-v2.component"; import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component"; import { AppearanceComponent } from "../vault/popup/settings/appearance.component"; @@ -407,6 +409,12 @@ const routes: Routes = [ }, ], }, + { + path: "assign-collections", + component: AssignCollections, + canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh, true, "/")], + data: { state: "assign-collections" }, + }, ...extensionRefreshSwap(AboutPageComponent, AboutPageV2Component, { path: "about", canActivate: [AuthGuard], @@ -450,12 +458,11 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { state: "tabs_settings" }, }), - { + ...extensionRefreshSwap(SendGroupingsComponent, SendV2Component, { path: "send", - component: SendGroupingsComponent, canActivate: [AuthGuard], data: { state: "tabs_send" }, - }, + }), ], }), { diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index e82eb429a5a..01470f4d115 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -20,18 +20,13 @@ import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services. import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; -import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; -import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; -import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; -import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AutofillSettingsService, @@ -47,6 +42,7 @@ import { } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { ClientType } from "@bitwarden/common/enums"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -55,6 +51,7 @@ import { FileDownloadService } from "@bitwarden/common/platform/abstractions/fil import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { @@ -65,6 +62,7 @@ import { BiometricStateService } from "@bitwarden/common/platform/biometrics/bio import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency injection import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; +import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; @@ -106,9 +104,11 @@ import BrowserLocalStorageService from "../../platform/services/browser-local-st import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; +import { ForegroundTaskSchedulerService } from "../../platform/services/task-scheduler/foreground-task-scheduler.service"; import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging"; +import { ForegroundVaultTimeoutService } from "../../services/vault-timeout/foreground-vault-timeout.service"; import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service"; import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service"; import { Fido2UserVerificationService } from "../../vault/services/fido2-user-verification.service"; @@ -168,11 +168,6 @@ const safeProviders: SafeProvider[] = [ useClass: UnauthGuardService, deps: [AuthService, Router], }), - safeProvider({ - provide: SsoLoginServiceAbstraction, - useFactory: getBgService("ssoLoginService"), - deps: [], - }), safeProvider({ provide: CryptoFunctionService, useFactory: () => new WebCryptoFunctionService(window), @@ -255,16 +250,6 @@ const safeProviders: SafeProvider[] = [ useClass: TotpService, deps: [CryptoFunctionService, LogService], }), - safeProvider({ - provide: DeviceTrustServiceAbstraction, - useFactory: getBgService("deviceTrustService"), - deps: [], - }), - safeProvider({ - provide: DevicesServiceAbstraction, - useFactory: getBgService("devicesService"), - deps: [], - }), safeProvider({ provide: OffscreenDocumentService, useClass: DefaultOffscreenDocumentService, @@ -330,6 +315,7 @@ const safeProviders: SafeProvider[] = [ ScriptInjectorService, AccountServiceAbstraction, AuthService, + ConfigService, MessageListener, ], }), @@ -338,25 +324,10 @@ const safeProviders: SafeProvider[] = [ useClass: BrowserScriptInjectorService, deps: [PlatformUtilsService, LogService], }), - safeProvider({ - provide: KeyConnectorService, - useFactory: getBgService("keyConnectorService"), - deps: [], - }), - safeProvider({ - provide: UserVerificationService, - useFactory: getBgService("userVerificationService"), - deps: [], - }), - safeProvider({ - provide: VaultTimeoutSettingsService, - useFactory: getBgService("vaultTimeoutSettingsService"), - deps: [], - }), safeProvider({ provide: VaultTimeoutService, - useFactory: getBgService("vaultTimeoutService"), - deps: [], + useClass: ForegroundVaultTimeoutService, + deps: [MessagingServiceAbstraction], }), safeProvider({ provide: NotificationsService, @@ -549,6 +520,15 @@ const safeProviders: SafeProvider[] = [ useClass: Fido2UserVerificationService, deps: [PasswordRepromptService, UserVerificationService, DialogService], }), + safeProvider({ + provide: TaskSchedulerService, + useExisting: ForegroundTaskSchedulerService, + }), + safeProvider({ + provide: ForegroundTaskSchedulerService, + useFactory: getBgService("taskSchedulerService"), + deps: [], + }), ]; @NgModule({ diff --git a/apps/browser/src/services/vault-timeout/foreground-vault-timeout.service.ts b/apps/browser/src/services/vault-timeout/foreground-vault-timeout.service.ts new file mode 100644 index 00000000000..462e2149e88 --- /dev/null +++ b/apps/browser/src/services/vault-timeout/foreground-vault-timeout.service.ts @@ -0,0 +1,18 @@ +import { VaultTimeoutService as BaseVaultTimeoutService } from "@bitwarden/common/src/abstractions/vault-timeout/vault-timeout.service"; +import { MessagingService } from "@bitwarden/common/src/platform/abstractions/messaging.service"; +import { UserId } from "@bitwarden/common/src/types/guid"; + +export class ForegroundVaultTimeoutService implements BaseVaultTimeoutService { + constructor(protected messagingService: MessagingService) {} + + // should only ever run in background + async checkVaultTimeout(): Promise {} + + async lock(userId?: UserId): Promise { + this.messagingService.send("lockVault", { userId }); + } + + async logOut(userId?: string): Promise { + this.messagingService.send("logout", { userId }); + } +} diff --git a/apps/browser/src/services/vault-timeout/vault-timeout.service.ts b/apps/browser/src/services/vault-timeout/vault-timeout.service.ts index 9e9a24fb9c3..e0b9db5422b 100644 --- a/apps/browser/src/services/vault-timeout/vault-timeout.service.ts +++ b/apps/browser/src/services/vault-timeout/vault-timeout.service.ts @@ -4,16 +4,13 @@ import { SafariApp } from "../../browser/safariApp"; export default class VaultTimeoutService extends BaseVaultTimeoutService { startCheck() { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.checkVaultTimeout(); if (this.platformUtilsService.isSafari()) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.checkSafari(); - } else { - setInterval(() => this.checkVaultTimeout(), 10 * 1000); // check every 10 seconds + this.checkVaultTimeout().catch((error) => this.logService.error(error)); + this.checkSafari().catch((error) => this.logService.error(error)); + return; } + + super.startCheck(); } // This is a work-around to safari adding an arbitrary delay to setTimeout and diff --git a/apps/browser/src/tools/popup/send/send-v2.component.html b/apps/browser/src/tools/popup/send/send-v2.component.html new file mode 100644 index 00000000000..3499f8c32ef --- /dev/null +++ b/apps/browser/src/tools/popup/send/send-v2.component.html @@ -0,0 +1,21 @@ + + + + + + + + + + +
+ + {{ "sendsNoItemsTitle" | i18n }} + {{ "sendsNoItemsMessage" | i18n }} + + +
+
diff --git a/apps/browser/src/tools/popup/send/send-v2.component.ts b/apps/browser/src/tools/popup/send/send-v2.component.ts new file mode 100644 index 00000000000..fba14b762b1 --- /dev/null +++ b/apps/browser/src/tools/popup/send/send-v2.component.ts @@ -0,0 +1,52 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { RouterLink } from "@angular/router"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { ButtonModule, NoItemsModule } from "@bitwarden/components"; +import { NoSendsIcon, NewSendDropdownComponent } from "@bitwarden/send-ui"; + +import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component"; +import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; +import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; + +enum SendsListState { + Empty, +} + +@Component({ + templateUrl: "send-v2.component.html", + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + PopOutComponent, + CurrentAccountComponent, + NoItemsModule, + JslibModule, + CommonModule, + ButtonModule, + RouterLink, + NewSendDropdownComponent, + ], +}) +export class SendV2Component implements OnInit, OnDestroy { + sendType = SendType; + + /** Visual state of the Sends list */ + protected sendsListState: SendsListState | null = null; + + protected noItemIcon = NoSendsIcon; + + protected SendsListStateEnum = SendsListState; + + constructor() { + this.sendsListState = SendsListState.Empty; + } + + ngOnInit(): void {} + + ngOnDestroy(): void {} +} diff --git a/apps/browser/src/vault/fido2/browser-fido2-user-interface.service.ts b/apps/browser/src/vault/fido2/browser-fido2-user-interface.service.ts index d4ad7209b79..df4f184f7ff 100644 --- a/apps/browser/src/vault/fido2/browser-fido2-user-interface.service.ts +++ b/apps/browser/src/vault/fido2/browser-fido2-user-interface.service.ts @@ -65,6 +65,7 @@ export type BrowserFido2Message = { sessionId: string } & ( type: "ConfirmNewCredentialRequest"; credentialName: string; userName: string; + userHandle: string; userVerification: boolean; fallbackSupported: boolean; rpId: string; @@ -242,6 +243,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi async confirmNewCredential({ credentialName, userName, + userHandle, userVerification, rpId, }: NewCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> { @@ -250,6 +252,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi sessionId: this.sessionId, credentialName, userName, + userHandle, userVerification, fallbackSupported: this.fallbackSupported, rpId, diff --git a/apps/browser/src/vault/popup/components/fido2/fido2.component.ts b/apps/browser/src/vault/popup/components/fido2/fido2.component.ts index 752a9100721..3a8c69cba95 100644 --- a/apps/browser/src/vault/popup/components/fido2/fido2.component.ts +++ b/apps/browser/src/vault/popup/components/fido2/fido2.component.ts @@ -143,8 +143,10 @@ export class Fido2Component implements OnInit, OnDestroy { this.ciphers = (await this.cipherService.getAllDecrypted()).filter( (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted, ); - this.displayedCiphers = this.ciphers.filter((cipher) => - cipher.login.matchesUri(this.url, equivalentDomains), + this.displayedCiphers = this.ciphers.filter( + (cipher) => + cipher.login.matchesUri(this.url, equivalentDomains) && + this.hasNoOtherPasskeys(cipher, message.userHandle), ); if (this.displayedCiphers.length > 0) { @@ -405,4 +407,18 @@ export class Fido2Component implements OnInit, OnDestroy { ...msg, }); } + + /** + * This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle + * @param userHandle + */ + private hasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean { + if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) { + return true; + } + + return cipher.login.fido2Credentials.some((passkey) => { + passkey.userHandle === userHandle; + }); + } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html index 4d8461a57c3..0ae2f0af01f 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html @@ -1,11 +1,16 @@ - + + + + + + + + + + + + + + + {{ "cancel" | i18n }} + + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts new file mode 100644 index 00000000000..a3ebadb7e2b --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts @@ -0,0 +1,81 @@ +import { CommonModule, Location } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ReactiveFormsModule } from "@angular/forms"; +import { ActivatedRoute } from "@angular/router"; +import { Observable, combineLatest, first, switchMap } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + ButtonModule, + CardComponent, + SelectModule, + FormFieldModule, + AsyncActionsModule, +} from "@bitwarden/components"; +import { AssignCollectionsComponent, CollectionAssignmentParams } from "@bitwarden/vault"; + +import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component"; +import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component"; +import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component"; + +@Component({ + standalone: true, + selector: "app-assign-collections", + templateUrl: "./assign-collections.component.html", + imports: [ + AsyncActionsModule, + ButtonModule, + CommonModule, + JslibModule, + SelectModule, + FormFieldModule, + AssignCollectionsComponent, + CardComponent, + ReactiveFormsModule, + PopupPageComponent, + PopupHeaderComponent, + PopupFooterComponent, + PopOutComponent, + ], +}) +export class AssignCollections { + /** Params needed to populate the assign collections component */ + params: CollectionAssignmentParams; + + constructor( + private location: Location, + private collectionService: CollectionService, + private cipherService: CipherService, + route: ActivatedRoute, + ) { + const $cipher: Observable = route.queryParams.pipe( + switchMap(({ cipherId }) => this.cipherService.get(cipherId)), + switchMap((cipherDomain) => + this.cipherService + .getKeyForCipherKeyDecryption(cipherDomain) + .then(cipherDomain.decrypt.bind(cipherDomain)), + ), + ); + + combineLatest([$cipher, this.collectionService.decryptedCollections$]) + .pipe(takeUntilDestroyed(), first()) + .subscribe(([cipherView, collections]) => { + this.params = { + ciphers: [cipherView], + organizationId: (cipherView?.organizationId as OrganizationId) ?? null, + availableCollections: collections.filter((c) => !c.readOnly), + }; + }); + } + + /** Navigates the user back to the previous screen */ + navigateBack() { + this.location.back(); + } +} diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.html index e7c59df21f4..b14e222bda3 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.html @@ -1,5 +1,10 @@ - + diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.spec.ts index 0f09d12db9f..342042c95f3 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.spec.ts @@ -26,6 +26,7 @@ import { CipherAttachmentsComponent } from "./cipher-attachments/cipher-attachme }) class MockPopupHeaderComponent { @Input() pageTitle: string; + @Input() backAction: () => void; } @Component({ diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.ts index da0def529c2..20e553ca748 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.ts @@ -1,4 +1,4 @@ -import { CommonModule } from "@angular/common"; +import { CommonModule, Location } from "@angular/common"; import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; @@ -41,6 +41,7 @@ export class AttachmentsV2Component { constructor( private router: Router, private cipherService: CipherService, + private location: Location, route: ActivatedRoute, ) { route.queryParams.pipe(takeUntilDestroyed(), first()).subscribe(({ cipherId }) => { @@ -48,6 +49,21 @@ export class AttachmentsV2Component { }); } + /** + * Navigates to previous view or edit-cipher path + * depending on the history length. + * + * This can happen when history is lost due to the extension being + * forced into a popout window. + */ + async handleBackButton() { + if (history.length === 1) { + await this.navigateToEditScreen(); + } else { + this.location.back(); + } + } + /** Navigate the user back to the edit screen after uploading an attachment */ async navigateToEditScreen() { const cipherDomain = await this.cipherService.get(this.cipherId); diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html index 05a6b54d4d0..0a0f44e8e00 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html @@ -28,9 +28,14 @@ {{ "clone" | i18n }} - + diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts index 4fe88da5550..91922162f96 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -102,7 +102,20 @@ export class ViewV2Component { return await cipher.decrypt(await this.cipherService.getKeyForCipherKeyDecryption(cipher)); } - editCipher() { + async checkForPasswordReprompt() { + this.passwordReprompted = + this.passwordReprompted || + (await this.passwordRepromptService.passwordRepromptCheck(this.cipher)); + if (!this.passwordReprompted) { + return false; + } + return true; + } + + async editCipher() { + if (!(await this.checkForPasswordReprompt())) { + return; + } if (this.cipher.isDeleted) { return false; } @@ -113,10 +126,7 @@ export class ViewV2Component { } delete = async (): Promise => { - this.passwordReprompted = - this.passwordReprompted || - (await this.passwordRepromptService.passwordRepromptCheck(this.cipher)); - if (!this.passwordReprompted) { + if (!(await this.checkForPasswordReprompt())) { return; } diff --git a/apps/browser/src/vault/popup/components/vault/current-tab.component.html b/apps/browser/src/vault/popup/components/vault/current-tab.component.html index 0b2e16d09d2..bb8a401da62 100644 --- a/apps/browser/src/vault/popup/components/vault/current-tab.component.html +++ b/apps/browser/src/vault/popup/components/vault/current-tab.component.html @@ -36,44 +36,6 @@
- -

- {{ unassignedItemsBannerService.bannerText$ | async | i18n }} - {{ "unassignedItemsBannerCTAPartOne" | i18n }} - {{ "adminConsole" | i18n }} - {{ "unassignedItemsBannerCTAPartTwo" | i18n }} - {{ "learnMore" | i18n }} -

- -

{{ "typeLogins" | i18n }} diff --git a/apps/browser/src/vault/popup/components/vault/current-tab.component.ts b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts index 97856a952ce..ec69330745f 100644 --- a/apps/browser/src/vault/popup/components/vault/current-tab.component.ts +++ b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts @@ -3,14 +3,11 @@ import { Router } from "@angular/router"; import { Subject, firstValueFrom, from, Subscription } from "rxjs"; import { debounceTime, switchMap, takeUntil } from "rxjs/operators"; -import { UnassignedItemsBannerService } from "@bitwarden/angular/services/unassigned-items-banner.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; 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 { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -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 { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -58,10 +55,6 @@ export class CurrentTabComponent implements OnInit, OnDestroy { private loadedTimeout: number; private searchTimeout: number; - protected unassignedItemsBannerEnabled$ = this.configService.getFeatureFlag$( - FeatureFlag.UnassignedItemsBanner, - ); - constructor( private platformUtilsService: PlatformUtilsService, private cipherService: CipherService, @@ -78,8 +71,6 @@ export class CurrentTabComponent implements OnInit, OnDestroy { private organizationService: OrganizationService, private vaultFilterService: VaultFilterService, private vaultSettingsService: VaultSettingsService, - private configService: ConfigService, - protected unassignedItemsBannerService: UnassignedItemsBannerService, ) {} async ngOnInit() { diff --git a/apps/browser/store/locales/bg/copy.resx b/apps/browser/store/locales/bg/copy.resx index 0ec0b6e3af9..5851b526ce9 100644 --- a/apps/browser/store/locales/bg/copy.resx +++ b/apps/browser/store/locales/bg/copy.resx @@ -118,7 +118,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - мениджър на пароли + Bitwarden — управител на пароли У дома, на работа или на път – Битуорден защитава всички Ваши пароли, секретни ключове и лична информация. diff --git a/apps/browser/tailwind.config.js b/apps/browser/tailwind.config.js index db1dd55694e..c0baf274a23 100644 --- a/apps/browser/tailwind.config.js +++ b/apps/browser/tailwind.config.js @@ -6,6 +6,7 @@ config.content = [ "../../libs/components/src/**/*.{html,ts}", "../../libs/auth/src/**/*.{html,ts}", "../../libs/angular/src/**/*.{html,ts}", + "../../libs/vault/src/**/*.{html,ts}", ]; module.exports = config; diff --git a/apps/browser/test.setup.ts b/apps/browser/test.setup.ts index 16ebdcbc605..2c358b62c4e 100644 --- a/apps/browser/test.setup.ts +++ b/apps/browser/test.setup.ts @@ -143,6 +143,18 @@ const webNavigation = { }, }; +const alarms = { + clear: jest.fn().mockImplementation((_name, callback) => callback(true)), + clearAll: jest.fn().mockImplementation((callback) => callback(true)), + create: jest.fn().mockImplementation((_name, _createInfo, callback) => callback()), + get: jest.fn().mockImplementation((_name, callback) => callback(null)), + getAll: jest.fn().mockImplementation((callback) => callback([])), + onAlarm: { + addListener: jest.fn(), + removeListener: jest.fn(), + }, +}; + // set chrome global.chrome = { i18n, @@ -158,4 +170,5 @@ global.chrome = { offscreen, permissions, webNavigation, + alarms, } as any; diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js index eb1244bc26d..e6ef80bcd9e 100644 --- a/apps/browser/webpack.config.js +++ b/apps/browser/webpack.config.js @@ -106,12 +106,27 @@ const plugins = [ chunks: ["notification/bar"], }), new HtmlWebpackPlugin({ - template: "./src/autofill/overlay/pages/button/button.html", + template: "./src/autofill/overlay/inline-menu/pages/button/button.html", + filename: "overlay/menu-button.html", + chunks: ["overlay/menu-button"], + }), + new HtmlWebpackPlugin({ + template: "./src/autofill/overlay/inline-menu/pages/list/list.html", + filename: "overlay/menu-list.html", + chunks: ["overlay/menu-list"], + }), + new HtmlWebpackPlugin({ + template: "./src/autofill/overlay/inline-menu/pages/menu-container/menu-container.html", + filename: "overlay/menu.html", + chunks: ["overlay/menu"], + }), + new HtmlWebpackPlugin({ + template: "./src/autofill/deprecated/overlay/pages/button/legacy-button.html", filename: "overlay/button.html", chunks: ["overlay/button"], }), new HtmlWebpackPlugin({ - template: "./src/autofill/overlay/pages/list/list.html", + template: "./src/autofill/deprecated/overlay/pages/list/legacy-list.html", filename: "overlay/list.html", chunks: ["overlay/list"], }), @@ -161,6 +176,8 @@ const mainConfig = { "./src/autofill/content/trigger-autofill-script-injection.ts", "content/bootstrap-autofill": "./src/autofill/content/bootstrap-autofill.ts", "content/bootstrap-autofill-overlay": "./src/autofill/content/bootstrap-autofill-overlay.ts", + "content/bootstrap-legacy-autofill-overlay": + "./src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts", "content/autofiller": "./src/autofill/content/autofiller.ts", "content/notificationBar": "./src/autofill/content/notification-bar.ts", "content/contextMenuHandler": "./src/autofill/content/context-menu-handler.ts", @@ -168,8 +185,16 @@ const mainConfig = { "content/fido2-content-script": "./src/autofill/fido2/content/fido2-content-script.ts", "content/fido2-page-script": "./src/autofill/fido2/content/fido2-page-script.ts", "notification/bar": "./src/autofill/notification/bar.ts", - "overlay/button": "./src/autofill/overlay/pages/button/bootstrap-autofill-overlay-button.ts", - "overlay/list": "./src/autofill/overlay/pages/list/bootstrap-autofill-overlay-list.ts", + "overlay/menu-button": + "./src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts", + "overlay/menu-list": + "./src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts", + "overlay/menu": + "./src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts", + "overlay/button": + "./src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts", + "overlay/list": + "./src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts", "encrypt-worker": "../../libs/common/src/platform/services/cryptography/encrypt.worker.ts", "content/lp-fileless-importer": "./src/tools/content/lp-fileless-importer.ts", "content/send-on-installed-message": "./src/vault/content/send-on-installed-message.ts", diff --git a/apps/cli/package.json b/apps/cli/package.json index 2822bd52ca7..2ae40a15ae4 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2024.7.0", + "version": "2024.7.1", "keywords": [ "bitwarden", "password", diff --git a/apps/cli/src/admin-console/models/response/organization-collection.response.ts b/apps/cli/src/admin-console/models/response/organization-collection.response.ts index ca47a4b0741..730e668909c 100644 --- a/apps/cli/src/admin-console/models/response/organization-collection.response.ts +++ b/apps/cli/src/admin-console/models/response/organization-collection.response.ts @@ -5,10 +5,12 @@ import { SelectionReadOnly } from "../selection-read-only"; export class OrganizationCollectionResponse extends CollectionResponse { groups: SelectionReadOnly[]; + users: SelectionReadOnly[]; - constructor(o: CollectionView, groups: SelectionReadOnly[]) { + constructor(o: CollectionView, groups: SelectionReadOnly[], users: SelectionReadOnly[]) { super(o); this.object = "org-collection"; this.groups = groups; + this.users = users; } } diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts index 75cd241207c..1bba149a35a 100644 --- a/apps/cli/src/commands/edit.command.ts +++ b/apps/cli/src/commands/edit.command.ts @@ -184,7 +184,7 @@ export class EditCommand { const response = await this.apiService.putCollection(req.organizationId, id, request); const view = CollectionExport.toView(req); view.id = response.id; - const res = new OrganizationCollectionResponse(view, groups); + const res = new OrganizationCollectionResponse(view, groups, users); return Response.success(res); } catch (e) { return Response.error(e); diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index a91df2a1caa..7e31750583b 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -456,7 +456,13 @@ export class GetCommand extends DownloadCommand { : response.groups.map( (g) => new SelectionReadOnly(g.id, g.readOnly, g.hidePasswords, g.manage), ); - const res = new OrganizationCollectionResponse(decCollection, groups); + const users = + response.users == null + ? null + : response.users.map( + (g) => new SelectionReadOnly(g.id, g.readOnly, g.hidePasswords, g.manage), + ); + const res = new OrganizationCollectionResponse(decCollection, groups, users); return Response.success(res); } catch (e) { return Response.error(e); diff --git a/apps/cli/src/service-container.ts b/apps/cli/src/service-container.ts index aeb233a31da..cfe310318fb 100644 --- a/apps/cli/src/service-container.ts +++ b/apps/cli/src/service-container.ts @@ -65,12 +65,17 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory import { MessageSender } from "@bitwarden/common/platform/messaging"; import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; +import { + TaskSchedulerService, + DefaultTaskSchedulerService, +} from "@bitwarden/common/platform/scheduling"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; +import { FallbackBulkEncryptService } from "@bitwarden/common/platform/services/cryptography/fallback-bulk-encrypt.service"; import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; @@ -239,6 +244,7 @@ export class ServiceContainer { providerApiService: ProviderApiServiceAbstraction; userAutoUnlockKeyService: UserAutoUnlockKeyService; kdfConfigService: KdfConfigServiceAbstraction; + taskSchedulerService: TaskSchedulerService; constructor() { let p = null; @@ -543,6 +549,7 @@ export class ServiceContainer { this.stateProvider, ); + this.taskSchedulerService = new DefaultTaskSchedulerService(this.logService); this.loginStrategyService = new LoginStrategyService( this.accountService, this.masterPasswordService, @@ -568,6 +575,7 @@ export class ServiceContainer { this.billingAccountProfileStateService, this.vaultTimeoutSettingsService, this.kdfConfigService, + this.taskSchedulerService, ); this.authService = new AuthService( @@ -586,6 +594,7 @@ export class ServiceContainer { this.environmentService, this.logService, this.stateProvider, + this.authService, ); this.cipherService = new CipherService( @@ -597,6 +606,7 @@ export class ServiceContainer { this.stateService, this.autofillSettingsService, this.encryptService, + new FallbackBulkEncryptService(this.encryptService), this.cipherFileUploadService, this.configService, this.stateProvider, @@ -641,6 +651,8 @@ export class ServiceContainer { this.authService, this.vaultTimeoutSettingsService, this.stateEventRunnerService, + this.taskSchedulerService, + this.logService, lockedCallback, null, ); @@ -723,6 +735,7 @@ export class ServiceContainer { this.stateProvider, this.logService, this.authService, + this.taskSchedulerService, ); this.eventCollectionService = new EventCollectionService( diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts index 716c2b42bb1..5db3bda97c2 100644 --- a/apps/cli/src/vault/create.command.ts +++ b/apps/cli/src/vault/create.command.ts @@ -208,7 +208,7 @@ export class CreateCommand { const response = await this.apiService.postCollection(req.organizationId, request); const view = CollectionExport.toView(req); view.id = response.id; - const res = new OrganizationCollectionResponse(view, groups); + const res = new OrganizationCollectionResponse(view, groups, users); return Response.success(res); } catch (e) { return Response.error(e); diff --git a/apps/desktop/package.json b/apps/desktop/package.json index f1639dc51a1..61d4607cea0 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2024.7.1", + "version": "2024.7.2", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index dfea2e6f274..c0b4bf4eb1c 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -45,6 +45,7 @@ import { BiometricStateService } from "@bitwarden/common/platform/biometrics/bio import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency injection import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; +import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { SystemService } from "@bitwarden/common/platform/services/system.service"; import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state"; @@ -177,6 +178,7 @@ const safeProviders: SafeProvider[] = [ VaultTimeoutSettingsService, BiometricStateService, AccountServiceAbstraction, + TaskSchedulerService, ], }), safeProvider({ diff --git a/apps/desktop/src/auth/two-factor-auth.component.ts b/apps/desktop/src/auth/two-factor-auth.component.ts index 191a88e621a..bb1ef601388 100644 --- a/apps/desktop/src/auth/two-factor-auth.component.ts +++ b/apps/desktop/src/auth/two-factor-auth.component.ts @@ -5,6 +5,8 @@ import { ReactiveFormsModule } from "@angular/forms"; import { RouterLink } from "@angular/router"; import { TwoFactorAuthAuthenticatorComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-authenticator.component"; +import { TwoFactorAuthEmailComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component"; +import { TwoFactorAuthWebAuthnComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component"; import { TwoFactorAuthYubikeyComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-yubikey.component"; import { TwoFactorAuthComponent as BaseTwoFactorAuthComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component"; import { TwoFactorOptionsComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-options.component"; @@ -35,8 +37,10 @@ import { TypographyModule } from "../../../../libs/components/src/typography"; RouterLink, CheckboxModule, TwoFactorOptionsComponent, + TwoFactorAuthEmailComponent, TwoFactorAuthAuthenticatorComponent, TwoFactorAuthYubikeyComponent, + TwoFactorAuthWebAuthnComponent, ], providers: [I18nPipe], }) diff --git a/apps/desktop/src/auth/two-factor.component.ts b/apps/desktop/src/auth/two-factor.component.ts index d1b84c1fa0e..3f5e8aee19f 100644 --- a/apps/desktop/src/auth/two-factor.component.ts +++ b/apps/desktop/src/auth/two-factor.component.ts @@ -25,6 +25,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { TwoFactorOptionsComponent } from "./two-factor-options.component"; @@ -64,6 +65,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { configService: ConfigService, masterPasswordService: InternalMasterPasswordServiceAbstraction, accountService: AccountService, + toastService: ToastService, @Inject(WINDOW) protected win: Window, ) { super( @@ -85,6 +87,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { configService, masterPasswordService, accountService, + toastService, ); super.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -149,6 +152,15 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { } override async launchDuoFrameless() { + if (this.duoFramelessUrl === null) { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("duoHealthCheckResultsInNullAuthUrlError"), + }); + return; + } + const duoHandOffMessage = { title: this.i18nService.t("youSuccessfullyLoggedIn"), message: this.i18nService.t("youMayCloseThisWindow"), diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index 3e643925e7a..84a9913c287 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Instellings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Vereis e-posbevestiging" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "U moet u e-pos bevestig om die funksie te gebruik." }, diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index b80ec1b4dc9..5885f86f253 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "الإعدادات" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "التحقق من البريد الإلكتروني مطلوب" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "يجب عليك التحقق من بريدك الإلكتروني لاستخدام هذه الميزة." }, diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 3481c03dfaa..b9dfef03424 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Ana parol" + }, + "masterPassImportant": { + "message": "Unutsanız, ana parolunuz geri qaytarıla bilməz!" + }, + "confirmMasterPassword": { + "message": "Ana parolu təsdiqlə" + }, + "masterPassHintLabel": { + "message": "Ana parol ipucusu" + }, "settings": { "message": "Ayarlar" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "E-poçtun doğrulanması tələb olunur" }, + "emailVerifiedV2": { + "message": "E-poçt doğrulandı" + }, "emailVerificationRequiredDesc": { "message": "Bu özəlliyi istifadə etmək üçün e-poçtunuzu doğrulamalısınız." }, diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index c1fa1119706..e729d9ca0a2 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Налады" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Патрабуецца праверка электроннай пошты" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Для выкарыстання гэтай функцыі патрабуецца праверыць электронную пошту." }, diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 23657992a5e..41ff64598e9 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Главна парола" + }, + "masterPassImportant": { + "message": "Главната парола не може да бъде възстановена, ако я забравите!" + }, + "confirmMasterPassword": { + "message": "Потвърждаване на главната парола" + }, + "masterPassHintLabel": { + "message": "Подсказка за главната парола" + }, "settings": { "message": "Настройки" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Изисква се потвърждение на е-пощата" }, + "emailVerifiedV2": { + "message": "Е-пощата е потвърдена" + }, "emailVerificationRequiredDesc": { "message": "Трябва да потвърдите е-пощата си, за да можете да използвате тази функционалност." }, diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index a383dd0a328..dd173da327f 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "সেটিংস" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index a3e5c4dc2c4..72ad7db55de 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Postavke" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 41ab3bba0a4..1d733b87a95 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Configuració" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Es requereix verificació per correu electrònic" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Heu de verificar el vostre correu electrònic per utilitzar aquesta característica." }, diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index dee3d75f694..59535060372 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Hlavní heslo" + }, + "masterPassImportant": { + "message": "Pokud zapomenete Vaše hlavní heslo, nebude možné jej obnovit!" + }, + "confirmMasterPassword": { + "message": "Potvrzení hlavního hesla" + }, + "masterPassHintLabel": { + "message": "Nápověda k hlavnímu heslu" + }, "settings": { "message": "Nastavení" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Je vyžadováno ověření e-mailu" }, + "emailVerifiedV2": { + "message": "E-mail byl ověřen" + }, "emailVerificationRequiredDesc": { "message": "Pro použití této funkce musíte ověřit svůj e-mail." }, diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 94f20c3e300..8d6988f73e3 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index bdb296ba717..8c20ec2c4a2 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Hovedadgangskode" + }, + "masterPassImportant": { + "message": "Hovedadgangskoden kan ikke gendannes, hvis den glemmes!" + }, + "confirmMasterPassword": { + "message": "Bekræft hovedadgangskode" + }, + "masterPassHintLabel": { + "message": "Hovedadgangskodetip" + }, "settings": { "message": "Indstillinger" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "E-mailbekræftelse kræves" }, + "emailVerifiedV2": { + "message": "E-mail bekræftet" + }, "emailVerificationRequiredDesc": { "message": "Du skal bekræfte din mailadresse for at bruge denne funktion." }, diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index ffd018bcc72..c52ff1d833a 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master-Passwort" + }, + "masterPassImportant": { + "message": "Dein Master-Passwort kann nicht wiederhergestellt werden, wenn du es vergisst!" + }, + "confirmMasterPassword": { + "message": "Master-Passwort bestätigen" + }, + "masterPassHintLabel": { + "message": "Master-Passwort-Hinweis" + }, "settings": { "message": "Einstellungen" }, @@ -667,17 +679,17 @@ "message": "Authenticator App" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Gib einen Code ein, der von einer Authentifizierungs-App wie dem Bitwarden Authenticator generiert wurde.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP security key" + "message": "Yubico OTP-Sicherheitsschlüssel" }, "yubiKeyDesc": { "message": "Verwende einen YubiKey, um auf dein Konto zuzugreifen. Funktioniert mit den Geräten YubiKey 4, Nano 4, 4C und NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Gib einen von Duo Security generierten Code ein.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -694,7 +706,7 @@ "message": "E-Mail" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Gib einen an deine E-Mail-Adresse gesendeten Code ein." }, "loginUnavailable": { "message": "Anmeldung nicht verfügbar" @@ -1685,13 +1697,13 @@ "message": "Deabonnieren" }, "atAnyTime": { - "message": "at any time." + "message": "jederzeit." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Indem du fortfährst, stimmst du den" }, "and": { - "message": "and" + "message": "und" }, "acceptPolicies": { "message": "Durch Anwählen dieses Kästchens erklärst du dich mit folgendem einverstanden:" @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "E-Mail-Verifizierung erforderlich" }, + "emailVerifiedV2": { + "message": "E-Mail-Adresse verifiziert" + }, "emailVerificationRequiredDesc": { "message": "Du musst deine E-Mail verifizieren, um diese Funktion nutzen zu können." }, @@ -2844,7 +2859,7 @@ "message": "Dateipasswort bestätigen" }, "exportSuccess": { - "message": "Vault data exported" + "message": "Tresor-Daten exportiert" }, "multifactorAuthenticationCancelled": { "message": "Multifaktor-Authentifizierung abgebrochen" diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 911a3735e6d..f1f950e3c22 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Ρυθμίσεις" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Απαιτείται Επαλήθευση Email" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Πρέπει να επαληθεύσετε το email σας για να χρησιμοποιήσετε αυτή τη δυνατότητα." }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 51543394827..c0ce5c17ee2 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -627,6 +627,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -2778,6 +2781,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 63d9cafd136..9c7aea781d4 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 6fa8989e863..5e404b77f09 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email Verification Required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index cbea97186fd..8bde0fc3c9b 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index a0fc028f830..f31fbe6ba82 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Ajustes" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Verificación de correo electrónico requerida" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Debes verificar tu correo electrónico para usar esta característica." }, diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index e799f8a2f40..4c3ba957ea4 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Seaded" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Vajalik on e-posti kinnitamine" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Enne selle funktsiooni kasutamist pead oma e-posti kinnitama." }, diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index e7347a53cf5..1ecfb02d902 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Ezarpenak" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Emailaren egiaztapena behar da" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Emaila egiaztatu behar duzu funtzio hau erabiltzeko." }, diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index aac7d569cb1..29cde5e0cad 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "تنظیمات" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "تأیید ایمیل لازم است" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "برای استفاده از این ویژگی باید ایمیل خود را تأیید کنید." }, diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index f42aa7d127a..2da6d22607c 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Pääsalasana" + }, + "masterPassImportant": { + "message": "Pääsalasanasi palauttaminen ei ole mahdollista, jos unohdat sen!" + }, + "confirmMasterPassword": { + "message": "Vahvista pääsalasana" + }, + "masterPassHintLabel": { + "message": "Pääsalasanan vihje" + }, "settings": { "message": "Asetukset" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Sähköpostiosoite on vahvistettava" }, + "emailVerifiedV2": { + "message": "Sähköpostiosoite on vahvistettu" + }, "emailVerificationRequiredDesc": { "message": "Sinun on vahvistettava sähköpostiosoitteesi käyttääksesi tätä ominaisuutta." }, diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 8eb371f8dcb..0d4d6e8c8ab 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Mga Preperensya" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Kailangan ang pag verify ng email" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Kailangan mong i verify ang iyong email upang magamit ang tampok na ito." }, diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 817f200ce1f..8bdbf19bfaa 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Paramètres" }, @@ -667,17 +679,17 @@ "message": "Application d'authentification" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Entrez un code généré par une application d'authentification comme Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP security key" + "message": "Clé de sécurité Yubico OTP" }, "yubiKeyDesc": { "message": "Utiliser une YubiKey pour accéder à votre compte. Fonctionne avec les appareils YubiKey 4, 4 Nano, 4C et NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Entrez un code généré par Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -694,7 +706,7 @@ "message": "Courriel" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Entrez le code envoyé par courriel." }, "loginUnavailable": { "message": "Identifiant non disponible" @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Vérification de courriel requise" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Vous devez vérifier votre courriel pour utiliser cette fonctionnalité." }, @@ -2157,7 +2172,7 @@ "message": "Export du coffre-fort de l'organisation" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Seul le coffre-fort de l'organisation associé à $ORGANIZATION$ sera exporté. Les coffres individuels ou d'autres organisations ne seront pas inclus.", "placeholders": { "organization": { "content": "$1", @@ -2230,7 +2245,7 @@ "message": "Générer un alias de courriel avec un service de transfert externe." }, "forwarderError": { - "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "message": "Erreur de $SERVICENAME$ : $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", "placeholders": { "servicename": { @@ -2258,7 +2273,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "Jeton d'API de $SERVICENAME$ non valide", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -2268,7 +2283,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "Jeton d'API de $SERVICENAME$ non valide : $ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -2282,7 +2297,7 @@ } }, "forwarderNoAccountId": { - "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "message": "Impossible d'obtenir l'ID de compte de messagerie masqué de $SERVICENAME$.", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -2302,7 +2317,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "URL de $SERVICENAME$ non valide.", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 0887d769823..fa4dcc02b87 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 8d82180f1de..ebad16e89c8 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "הגדרות" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "נדרש כתובת אימייל לאימות" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "נדרש אישור אימות בדוא\"ל כדי לאפשר שימוש בתכונה זו." }, diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 78367ecd0dd..d894e487d30 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 6d26e9c3c6b..67b671ed196 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Postavke" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Potrebna je potvrda e-pošte" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Za korištenje ove značajke, potrebna je ovjera e-pošte." }, diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 14c97b81fe5..3ffdd7ef1e6 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Mesterjelszó" + }, + "masterPassImportant": { + "message": "A mesterjelszó nem állítható helyre, ha elfelejtik!" + }, + "confirmMasterPassword": { + "message": "Mesterjelszó megerősítése" + }, + "masterPassHintLabel": { + "message": "Mesterjelszó emlékeztető" + }, "settings": { "message": "Beállítások" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email hitelesítés szükséges" }, + "emailVerifiedV2": { + "message": "Az email cím ellenőrzésre került." + }, "emailVerificationRequiredDesc": { "message": "A funkció használatához ellenőrizni kell az email címet." }, diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 574ab5bb6b2..6a45046ea59 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Setelan" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Verifikasi Email Diperlukan" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Anda harus mengkonfirmasi email anda untuk menggunakan fitur ini." }, diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index c0c1e322c65..95131aa47dc 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Impostazioni" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Verifica email obbligatoria" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Devi verificare la tua email per usare questa funzionalità." }, diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index 80060295411..bffd5de8396 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "マスターパスワード" + }, + "masterPassImportant": { + "message": "マスターパスワードを忘れた場合は復元できません!" + }, + "confirmMasterPassword": { + "message": "マスターパスワードの確認" + }, + "masterPassHintLabel": { + "message": "マスターパスワードのヒント" + }, "settings": { "message": "設定" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "メールアドレスの確認が必要です" }, + "emailVerifiedV2": { + "message": "メールアドレスを認証しました" + }, "emailVerificationRequiredDesc": { "message": "この機能を使用するにはメールアドレスを確認する必要があります。" }, diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index 0887d769823..fa4dcc02b87 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 0887d769823..fa4dcc02b87 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index ae4d9150af5..9866dafb8fe 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "ಸೆಟ್ಟಿಂಗ್‍ಗಳು" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "ಇಮೇಲ್ ಪರಿಶೀಲನೆ ಅಗತ್ಯವಿದೆ" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "ಈ ವೈಶಿಷ್ಟ್ಯವನ್ನು ಬಳಸಲು ನಿಮ್ಮ ಇಮೇಲ್ ಅನ್ನು ನೀವು ಪರಿಶೀಲಿಸಬೇಕು." }, diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 63d03f5ebf9..c2f3437f608 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "설정" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "이메일 인증 필요함" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "이 기능을 이용하기 위해서는 이메일을 인증해야 합니다." }, diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index e1020f7d237..77e9f40e25b 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Nustatymai" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Reikalingas elektroninio pašto patvirtinimas" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Privalote patvirtinti savo el. paštą norint naudotis šia funkcija." }, diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 64c27f9f627..fe6aad78be7 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Galvenā parole" + }, + "masterPassImportant": { + "message": "Galveno paroli nevar atgūt, ja tā tiek aizmirsta." + }, + "confirmMasterPassword": { + "message": "Apstiprināt galveno paroli" + }, + "masterPassHintLabel": { + "message": "Galvenās paroles norāde" + }, "settings": { "message": "Iestatījumi" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Nepieciešama e-pasta adreses apstiprināšana" }, + "emailVerifiedV2": { + "message": "E-pasta adrese ir apliecināta" + }, "emailVerificationRequiredDesc": { "message": "Ir jāapstiprina e-pasta adrese, lai izmantotu šo iespēju." }, diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index 6e02878b12d..71e9fe25948 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Podešavanja" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 880cc5b0848..6bd7a323d73 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "ക്രമീകരണങ്ങള്‍" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 0887d769823..fa4dcc02b87 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index d007a4e0f45..90632de6251 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index 03d6c95a293..e0f5fa0d532 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Innstillinger" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "E-postbekreftelse kreves" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Du må bekrefte E-postadressen din for å bruke denne funksjonen." }, diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 560a5c41c70..a023d51c1b7 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index 796b23b0c36..3ea2be8da9b 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Hoofdwachtwoord" + }, + "masterPassImportant": { + "message": "Je kunt je hoofdwachtwoord niet herstellen als je het vergeet!" + }, + "confirmMasterPassword": { + "message": "Hoofdwachtwoord bevestigen" + }, + "masterPassHintLabel": { + "message": "Hoofdwachtwoordhint" + }, "settings": { "message": "Instellingen" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "E-mailverificatie vereist" }, + "emailVerifiedV2": { + "message": "E-mailadres geverifieerd" + }, "emailVerificationRequiredDesc": { "message": "Je moet je e-mailadres verifiëren om deze functionaliteit te gebruiken." }, diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index f6d4a6ed3c4..e8cea993671 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Innstillingar" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index d6781321446..a939637ea8a 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index ead83d7fa7c..18f4c0bf0db 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Hasło główne" + }, + "masterPassImportant": { + "message": "Twoje hasło główne nie może zostać odzyskane, jeśli je zapomnisz!" + }, + "confirmMasterPassword": { + "message": "Potwierdź hasło główne" + }, + "masterPassHintLabel": { + "message": "Podpowiedź do hasła głównego" + }, "settings": { "message": "Ustawienia" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Weryfikacja adresu e-mail jest wymagana" }, + "emailVerifiedV2": { + "message": "E-mail zweryfikowany" + }, "emailVerificationRequiredDesc": { "message": "Musisz zweryfikować adres e-mail, aby używać tej funkcji." }, diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index de5ed3fc600..3b9d8406840 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Configurações" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Verificação de E-mail Necessária" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Você precisa verificar o seu e-mail para usar este recurso." }, diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index a1b9d5d6238..dc7a6226849 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -346,7 +346,7 @@ "message": "Remover" }, "nameRequired": { - "message": "É necessário o nome." + "message": "O nome é obrigatório." }, "addedItem": { "message": "Item adicionado" @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Palavra-passe mestra" + }, + "masterPassImportant": { + "message": "A sua palavra-passe mestra não pode ser recuperada se a esquecer!" + }, + "confirmMasterPassword": { + "message": "Confirmar a palavra-passe mestra" + }, + "masterPassHintLabel": { + "message": "Dica da palavra-passe mestra" + }, "settings": { "message": "Definições" }, @@ -1944,7 +1956,7 @@ "message": "Eliminação pendente" }, "webAuthnAuthenticate": { - "message": "Autenticar WebAuthn" + "message": "Autenticar o WebAuthn" }, "hideEmail": { "message": "Ocultar o meu endereço de e-mail dos destinatários." @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Verificação de e-mail necessária" }, + "emailVerifiedV2": { + "message": "E-mail verificado" + }, "emailVerificationRequiredDesc": { "message": "É necessário verificar o seu e-mail para utilizar esta funcionalidade." }, @@ -2094,10 +2109,10 @@ } }, "leaveOrganization": { - "message": "Deixar a organização" + "message": "Sair da organização" }, "leaveOrganizationConfirmation": { - "message": "Tem a certeza de que pretende deixar esta organização?" + "message": "Tem a certeza de que pretende sair desta organização?" }, "leftOrganization": { "message": "Saiu da organização." @@ -2606,10 +2621,10 @@ "message": "Dispositivo de confiança" }, "inputRequired": { - "message": "Campo necessário." + "message": "Campo obrigatório." }, "required": { - "message": "necessário" + "message": "obrigatório" }, "search": { "message": "Procurar" diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 874240889ed..2dcc23073e4 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Setări" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Verificare e-mail necesară" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Trebuie să vă verificați e-mailul pentru a utiliza această caracteristică." }, diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 92330eeb3aa..ecfb9437124 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Мастер-пароль" + }, + "masterPassImportant": { + "message": "Ваш мастер-пароль невозможно восстановить, если вы его забудете!" + }, + "confirmMasterPassword": { + "message": "Подтвердите мастер-пароль" + }, + "masterPassHintLabel": { + "message": "Подсказка к мастер-паролю" + }, "settings": { "message": "Настройки" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Требуется подтверждение электронной почты" }, + "emailVerifiedV2": { + "message": "Email подтвержден" + }, "emailVerificationRequiredDesc": { "message": "Для использования этой функции необходимо подтвердить свою электронную почту." }, diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 906181c7bdb..0ac4b332235 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "සැකසුම්" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 42ebe6965b3..06ef1668103 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Hlavné heslo" + }, + "masterPassImportant": { + "message": "Vaše hlavné heslo sa nebude dať obnoviť, ak ho zabudnete!" + }, + "confirmMasterPassword": { + "message": "Potvrdiť hlavné heslo" + }, + "masterPassHintLabel": { + "message": "Nápoveda pre hlavné heslo" + }, "settings": { "message": "Nastavenia" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Vyžaduje sa overenie e-mailu" }, + "emailVerifiedV2": { + "message": "Overený e-mail" + }, "emailVerificationRequiredDesc": { "message": "Na používanie tejto funkcie musíte overiť svoj e-mail." }, diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index e275b396620..9fc6f48f400 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Nastavitve" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index e254b694c34..33b4557f27b 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Главна лозинка" + }, + "masterPassImportant": { + "message": "Ваша главна лозинка се не може повратити ако је заборавите!" + }, + "confirmMasterPassword": { + "message": "Потрдити главну лозинку" + }, + "masterPassHintLabel": { + "message": "Савет главне лозинке" + }, "settings": { "message": "Подешавања" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Потребна је верификација е-поште" }, + "emailVerifiedV2": { + "message": "Имејл верификован" + }, "emailVerificationRequiredDesc": { "message": "Морате да проверите е-пошту да бисте користили ову функцију." }, diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index 3e73b2598b7..b90451b78a1 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Huvudlösenord" + }, + "masterPassImportant": { + "message": "Ditt huvudlösenord kan inte återställas om du glömmer det!" + }, + "confirmMasterPassword": { + "message": "Bekräfta huvudlösenord" + }, + "masterPassHintLabel": { + "message": "Huvudlösenordsledtråd" + }, "settings": { "message": "Inställningar" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "E-postverifiering krävs" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Du måste verifiera din e-post för att använda den här funktionen." }, diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 0887d769823..fa4dcc02b87 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index 8c0a06ec424..a0df28b7c6e 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "การตั้งค่า" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index b44135e8d43..c3a100394c9 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Ana parola" + }, + "masterPassImportant": { + "message": "Ana parolanızı unutursanız kurtaramazsınız!" + }, + "confirmMasterPassword": { + "message": "Ana parolayı onaylayın" + }, + "masterPassHintLabel": { + "message": "Ana parola ipucu" + }, "settings": { "message": "Ayarlar" }, @@ -1349,28 +1361,28 @@ "message": "This file export will be password protected and require the file password to decrypt." }, "filePassword": { - "message": "File password" + "message": "Dosya parolası" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "Bu parola, bu dosyayı dışa ve içe aktarmak için kullanılacaktır" }, "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": "Dışa aktarmayı şifrelemek ve içe aktarmayı yalnızca mevcut Bitwarden hesabıyla kısıtlamak için, hesabınızın kullanıcı adı ve ana parolasından türetilen hesap şifreleme anahtarınızı kullanın." }, "passwordProtected": { - "message": "Password protected" + "message": "Parola korumalı" }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "Dışa aktardığınız dosyayı şifrelemek ve bir Bitwarden hesabına içe aktarmak için kullanacağınız parolayı belirleyin." }, "exportTypeHeading": { - "message": "Export type" + "message": "Dışa aktarma türü" }, "accountRestricted": { - "message": "Account restricted" + "message": "Hesap kısıtlı" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "\"Dosya parolası\" ile \"Dosya parolasını onaylayın\" eşleşmiyor." }, "hCaptchaUrl": { "message": "hCaptcha adresi", @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "E-posta doğrulaması gerekiyor" }, + "emailVerifiedV2": { + "message": "E-posta doğrulandı" + }, "emailVerificationRequiredDesc": { "message": "Bu özelliği kullanmak için e-postanızı doğrulamalısınız." }, @@ -2133,7 +2148,7 @@ "message": "Hesabı değiştir" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Zaten hesabınız var mı?" }, "options": { "message": "Seçenekler" @@ -2154,7 +2169,7 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "Kuruluş kasasını dışa aktarma" }, "exportingOrganizationVaultDesc": { "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", @@ -2482,25 +2497,25 @@ "message": "Giriş istendi" }, "creatingAccountOn": { - "message": "Creating account on" + "message": "Hesap oluşturuluyor:" }, "checkYourEmail": { - "message": "Check your email" + "message": "E-postanızı kontrol edin" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "Hesabınızı oluşturmaya devam etmek için" }, "andContinueCreatingYourAccount": { - "message": "and continue creating your account." + "message": "adresine gönderdiğimiz e-postadaki bağlantıya tıklayın." }, "noEmail": { - "message": "No email?" + "message": "E-posta gelmedi mi?" }, "goBack": { - "message": "Go back" + "message": "Geri dönüp" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "e-posta adresinizi düzenleyin." }, "exposedMasterPassword": { "message": "Açığa Çıkmış Ana Parola" @@ -2968,11 +2983,11 @@ } }, "back": { - "message": "Back", + "message": "Geri", "description": "Button text to navigate back" }, "removeItem": { - "message": "Remove $NAME$", + "message": "$NAME$ klasörünü kaldır", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 9027b8d627c..3b3d5263153 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Головний пароль" + }, + "masterPassImportant": { + "message": "Головний пароль неможливо відновити, якщо ви його втратите!" + }, + "confirmMasterPassword": { + "message": "Підтвердьте головний пароль" + }, + "masterPassHintLabel": { + "message": "Підказка для головного пароля" + }, "settings": { "message": "Налаштування" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Необхідно підтвердити е-пошту" }, + "emailVerifiedV2": { + "message": "Електронну пошту підтверджено" + }, "emailVerificationRequiredDesc": { "message": "Для використання цієї функції необхідно підтвердити електронну пошту." }, diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index 6e155b82a75..26f7feec398 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Cài đặt" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Yêu cầu xác nhận danh tính qua Email" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Bạn phải xác minh email của mình để sử dụng tính năng này." }, diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 0d25428e768..a874b2ff5ef 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "主密码" + }, + "masterPassImportant": { + "message": "主密码忘记后,将无法恢复!" + }, + "confirmMasterPassword": { + "message": "确认主密码" + }, + "masterPassHintLabel": { + "message": "主密码提示" + }, "settings": { "message": "设置" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "需要验证电子邮件" }, + "emailVerifiedV2": { + "message": "电子邮箱已验证" + }, "emailVerificationRequiredDesc": { "message": "您必须验证您的电子邮件才能使用此功能。" }, diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 75ac7dfb3fe..427997aa543 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "設定" }, @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "需要驗證電子郵件" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "必須驗證您的電子郵件才能使用此功能。" }, diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 4ba7c6b6336..d3d99c4cfb3 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.7.1", + "version": "2024.7.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.7.1", + "version": "2024.7.2", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native", diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 1793642dab6..a6bd0d9ef39 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2024.7.1", + "version": "2024.7.2", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/desktop/src/scss/box.scss b/apps/desktop/src/scss/box.scss index 0e89e9fd74e..8c15aa91c62 100644 --- a/apps/desktop/src/scss/box.scss +++ b/apps/desktop/src/scss/box.scss @@ -221,6 +221,7 @@ .txt-right { float: right; margin-left: 10px; + width: 100px; } .row-main { diff --git a/apps/web/package.json b/apps/web/package.json index 11bf27b4e39..b75d9eac4c2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.7.0", + "version": "2024.7.1", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/apps/web/src/app/admin-console/common/new-base.people.component.ts b/apps/web/src/app/admin-console/common/base-members.component.ts similarity index 97% rename from apps/web/src/app/admin-console/common/new-base.people.component.ts rename to apps/web/src/app/admin-console/common/base-members.component.ts index 90c25e840c0..c13bec78c56 100644 --- a/apps/web/src/app/admin-console/common/new-base.people.component.ts +++ b/apps/web/src/app/admin-console/common/base-members.component.ts @@ -4,7 +4,6 @@ import { FormControl } from "@angular/forms"; import { firstValueFrom, lastValueFrom, debounceTime, combineLatest, BehaviorSubject } from "rxjs"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; import { @@ -35,7 +34,7 @@ export type UserViewTypes = ProviderUserUserDetailsResponse | OrganizationUserVi * This will replace BasePeopleComponent once all subclasses have been changed over to use this class. */ @Directive() -export abstract class NewBasePeopleComponent { +export abstract class BaseMembersComponent { /** * Shows a banner alerting the admin that users need to be confirmed. */ @@ -52,6 +51,10 @@ export abstract class NewBasePeopleComponent { return this.dataSource.acceptedUserCount > 0; } + get showBulkReinviteUsers(): boolean { + return this.dataSource.invitedUserCount > 0; + } + abstract userType: typeof OrganizationUserType | typeof ProviderUserType; abstract userStatusType: typeof OrganizationUserStatusType | typeof ProviderUserStatusType; @@ -77,7 +80,6 @@ export abstract class NewBasePeopleComponent { protected i18nService: I18nService, protected cryptoService: CryptoService, protected validationService: ValidationService, - protected modalService: ModalService, private logService: LogService, protected userNamePipe: UserNamePipe, protected dialogService: DialogService, diff --git a/apps/web/src/app/admin-console/common/people-table-data-source.ts b/apps/web/src/app/admin-console/common/people-table-data-source.ts index db357b4dbcd..5ce7e7bda7d 100644 --- a/apps/web/src/app/admin-console/common/people-table-data-source.ts +++ b/apps/web/src/app/admin-console/common/people-table-data-source.ts @@ -4,7 +4,7 @@ import { } from "@bitwarden/common/admin-console/enums"; import { TableDataSource } from "@bitwarden/components"; -import { StatusType, UserViewTypes } from "./new-base.people.component"; +import { StatusType, UserViewTypes } from "./base-members.component"; const MaxCheckedCount = 500; diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html index eaf10405dbf..3151c303ec9 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html @@ -13,7 +13,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}

diff --git a/apps/web/src/app/admin-console/organizations/manage/groups.component.html b/apps/web/src/app/admin-console/organizations/manage/groups.component.html index 1a1a7cdb904..2ebafb38fc9 100644 --- a/apps/web/src/app/admin-console/organizations/manage/groups.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/groups.component.html @@ -16,7 +16,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}

{{ "noGroupsInList" | i18n }}

diff --git a/apps/web/src/app/admin-console/organizations/manage/new-groups.component.html b/apps/web/src/app/admin-console/organizations/manage/new-groups.component.html index 3e659e5b6a8..1254d48cc76 100644 --- a/apps/web/src/app/admin-console/organizations/manage/new-groups.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/new-groups.component.html @@ -16,7 +16,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}

{{ "noGroupsInList" | i18n }}

diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component.ts new file mode 100644 index 00000000000..8d634c38e05 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component.ts @@ -0,0 +1,99 @@ +import { Directive, OnInit } from "@angular/core"; + +import { + OrganizationUserBulkPublicKeyResponse, + OrganizationUserBulkResponse, +} from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; +import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk-public-key.response"; +import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; + +import { BulkUserDetails } from "./bulk-status.component"; + +@Directive() +export abstract class BaseBulkConfirmComponent implements OnInit { + protected users: BulkUserDetails[]; + + protected excludedUsers: BulkUserDetails[]; + protected filteredUsers: BulkUserDetails[]; + + protected publicKeys: Map = new Map(); + protected fingerprints: Map = new Map(); + protected statuses: Map = new Map(); + + protected done = false; + protected loading = true; + protected error: string; + + protected constructor( + protected cryptoService: CryptoService, + protected i18nService: I18nService, + ) {} + + async ngOnInit() { + this.excludedUsers = this.users.filter((user) => !this.isAccepted(user)); + this.filteredUsers = this.users.filter((user) => this.isAccepted(user)); + + if (this.filteredUsers.length <= 0) { + this.done = true; + } + + const publicKeysResponse = await this.getPublicKeys(); + + for (const entry of publicKeysResponse.data) { + const publicKey = Utils.fromB64ToArray(entry.key); + const fingerprint = await this.cryptoService.getFingerprint(entry.userId, publicKey); + if (fingerprint != null) { + this.publicKeys.set(entry.id, publicKey); + this.fingerprints.set(entry.id, fingerprint.join("-")); + } + } + + this.loading = false; + } + + submit = async () => { + this.loading = true; + try { + const key = await this.getCryptoKey(); + const userIdsWithKeys: { id: string; key: string }[] = []; + + for (const user of this.filteredUsers) { + const publicKey = this.publicKeys.get(user.id); + if (publicKey == null) { + continue; + } + const encryptedKey = await this.cryptoService.rsaEncrypt(key.key, publicKey); + userIdsWithKeys.push({ + id: user.id, + key: encryptedKey.encryptedString, + }); + } + + const userBulkResponse = await this.postConfirmRequest(userIdsWithKeys); + + userBulkResponse.data.forEach((entry) => { + const error = entry.error !== "" ? entry.error : this.i18nService.t("bulkConfirmMessage"); + this.statuses.set(entry.id, error); + }); + + this.done = true; + } catch (e) { + this.error = e.message; + } + this.loading = false; + }; + + protected abstract getCryptoKey(): Promise; + protected abstract getPublicKeys(): Promise< + ListResponse + >; + protected abstract isAccepted(user: BulkUserDetails): boolean; + protected abstract postConfirmRequest( + userIdsWithKeys: { id: string; key: string }[], + ): Promise>; +} diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-remove.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-remove.component.ts new file mode 100644 index 00000000000..6c736346604 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-remove.component.ts @@ -0,0 +1,40 @@ +import { Directive } from "@angular/core"; + +import { OrganizationUserBulkResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; +import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +@Directive() +export abstract class BaseBulkRemoveComponent { + protected showNoMasterPasswordWarning: boolean; + protected statuses: Map = new Map(); + + protected done = false; + protected loading = false; + protected error: string; + + protected constructor(protected i18nService: I18nService) {} + + submit = async () => { + this.loading = true; + try { + const deleteUsersResponse = await this.deleteUsers(); + deleteUsersResponse.data.forEach((entry) => { + const error = entry.error !== "" ? entry.error : this.i18nService.t("bulkRemovedMessage"); + this.statuses.set(entry.id, error); + }); + this.done = true; + } catch (e) { + this.error = e.message; + } + + this.loading = false; + }; + + protected abstract deleteUsers(): Promise< + ListResponse + >; + + protected abstract get removeUsersWarning(): string; +} diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts index ffaf27ea46d..dba6319b273 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts @@ -33,7 +33,7 @@ type BulkStatusDialogData = { users: Array; filteredUsers: Array; request: Promise>; - successfullMessage: string; + successfulMessage: string; }; @Component({ @@ -67,7 +67,7 @@ export class BulkStatusComponent implements OnInit { ); this.users = data.users.map((user) => { - let message = keyedErrors[user.id] ?? data.successfullMessage; + let message = keyedErrors[user.id] ?? data.successfulMessage; // eslint-disable-next-line if (!keyedFilteredUsers.hasOwnProperty(user.id)) { message = this.i18nService.t("bulkFilteredMessage"); diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.html b/apps/web/src/app/admin-console/organizations/members/members.component.html index 99afe8099a6..ae80130e03b 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.html +++ b/apps/web/src/app/admin-console/organizations/members/members.component.html @@ -5,7 +5,7 @@ [placeholder]="'searchMembers' | i18n" > - @@ -52,7 +52,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}

{{ "noMembersInList" | i18n }}

@@ -103,7 +103,12 @@
- diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 93827539f8f..809f1e3935d 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -45,7 +45,7 @@ import { Collection } from "@bitwarden/common/vault/models/domain/collection"; import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response"; import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components"; -import { NewBasePeopleComponent } from "../../common/new-base.people.component"; +import { BaseMembersComponent } from "../../common/base-members.component"; import { PeopleTableDataSource } from "../../common/people-table-data-source"; import { GroupService } from "../core"; import { OrganizationUserView } from "../core/views/organization-user.view"; @@ -70,7 +70,7 @@ class MembersTableDataSource extends PeopleTableDataSource @Component({ templateUrl: "members.component.html", }) -export class MembersComponent extends NewBasePeopleComponent { +export class MembersComponent extends BaseMembersComponent { @ViewChild("resetPasswordTemplate", { read: ViewContainerRef, static: true }) resetPasswordModalRef: ViewContainerRef; @@ -94,7 +94,6 @@ export class MembersComponent extends NewBasePeopleComponent -
diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.spec.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.spec.ts index 90e652675c4..592995f88fc 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.spec.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.spec.ts @@ -205,7 +205,7 @@ describe("AccessSelectorComponent", () => { labelName: "Member 1", listName: "Member 1 (member1@email.com)", email: "member1@email.com", - role: OrganizationUserType.Manager, + role: OrganizationUserType.User, status: OrganizationUserStatusType.Confirmed, }, ]; diff --git a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html index e7eb29a3ac7..7640e1c7366 100644 --- a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html +++ b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html @@ -7,7 +7,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}

diff --git a/apps/web/src/app/admin-console/providers/providers.component.html b/apps/web/src/app/admin-console/providers/providers.component.html index d07342c85c2..560c164415c 100644 --- a/apps/web/src/app/admin-console/providers/providers.component.html +++ b/apps/web/src/app/admin-console/providers/providers.component.html @@ -3,7 +3,7 @@

- {{ "loading" | i18n }} + {{ "loading" | i18n }}

@@ -20,7 +20,7 @@ title="{{ 'providerIsDisabled' | i18n }}" aria-hidden="true" > - {{ "providerIsDisabled" | i18n }} + {{ "providerIsDisabled" | i18n }} diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts index 7906731d81e..6f68821943b 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts @@ -4,6 +4,8 @@ import mock from "jest-mock-extended/lib/Mock"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { UserKeyResponse } from "@bitwarden/common/models/response/user-key.response"; +import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -31,15 +33,18 @@ describe("EmergencyAccessService", () => { let apiService: MockProxy; let cryptoService: MockProxy; let encryptService: MockProxy; + let bulkEncryptService: MockProxy; let cipherService: MockProxy; let logService: MockProxy; let emergencyAccessService: EmergencyAccessService; + let configService: ConfigService; beforeAll(() => { emergencyAccessApiService = mock(); apiService = mock(); cryptoService = mock(); encryptService = mock(); + bulkEncryptService = mock(); cipherService = mock(); logService = mock(); @@ -48,8 +53,10 @@ describe("EmergencyAccessService", () => { apiService, cryptoService, encryptService, + bulkEncryptService, cipherService, logService, + configService, ); }); diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts index 362b1dec3cc..5b9d73c75e5 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts @@ -9,6 +9,9 @@ import { KdfConfig, PBKDF2KdfConfig, } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -45,8 +48,10 @@ export class EmergencyAccessService private apiService: ApiService, private cryptoService: CryptoService, private encryptService: EncryptService, + private bulkEncryptService: BulkEncryptService, private cipherService: CipherService, private logService: LogService, + private configService: ConfigService, ) {} /** @@ -225,10 +230,18 @@ export class EmergencyAccessService ); const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey; - const ciphers = await this.encryptService.decryptItems( - response.ciphers.map((c) => new Cipher(c)), - grantorUserKey, - ); + let ciphers: CipherView[] = []; + if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) { + ciphers = await this.bulkEncryptService.decryptItems( + response.ciphers.map((c) => new Cipher(c)), + grantorUserKey, + ); + } else { + ciphers = await this.encryptService.decryptItems( + response.ciphers.map((c) => new Cipher(c)), + grantorUserKey, + ); + } return ciphers.sort(this.cipherService.getLocaleSortingFunction()); } diff --git a/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.html b/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.html index ed59cc12388..615edb82d0c 100644 --- a/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.html +++ b/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.html @@ -13,7 +13,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}

diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.component.html b/apps/web/src/app/auth/organization-invite/accept-organization.component.html index 04258e7a46a..88eaa37e8d2 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.component.html +++ b/apps/web/src/app/auth/organization-invite/accept-organization.component.html @@ -7,7 +7,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}

diff --git a/apps/web/src/app/auth/settings/account/profile.component.html b/apps/web/src/app/auth/settings/account/profile.component.html index 4464824c63e..93025420b26 100644 --- a/apps/web/src/app/auth/settings/account/profile.component.html +++ b/apps/web/src/app/auth/settings/account/profile.component.html @@ -19,8 +19,8 @@
-
- +
+
diff --git a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html index fb7c774d6db..71a4ff119c2 100644 --- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html +++ b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html @@ -4,7 +4,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }} {{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }} diff --git a/apps/web/src/app/billing/individual/premium.component.ts b/apps/web/src/app/billing/individual/premium.component.ts index 071cefd7a5f..7b1e46805bd 100644 --- a/apps/web/src/app/billing/individual/premium.component.ts +++ b/apps/web/src/app/billing/individual/premium.component.ts @@ -108,8 +108,8 @@ export class PremiumComponent implements OnInit { fd.append("paymentToken", result[0]); } fd.append("additionalStorageGb", (this.additionalStorage || 0).toString()); - fd.append("country", this.taxInfoComponent.taxInfo.country); - fd.append("postalCode", this.taxInfoComponent.taxInfo.postalCode); + fd.append("country", this.taxInfoComponent?.taxFormGroup?.value.country); + fd.append("postalCode", this.taxInfoComponent?.taxFormGroup?.value.postalCode); return this.apiService.postPremium(fd); }) .then((paymentResponse) => { diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.html b/apps/web/src/app/billing/organizations/organization-plans.component.html index b9a3cc6bf05..5f34ef6cb95 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.html +++ b/apps/web/src/app/billing/organizations/organization-plans.component.html @@ -19,12 +19,13 @@
{{ "licenseFileDesc" | i18n: "bitwarden_organization_license.json" }} diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index 606440f2be6..e519b8887b1 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -3,7 +3,7 @@ - {{ "loading" | i18n }} + {{ "loading" | i18n }} - {{ "loading" | i18n }} + {{ "loading" | i18n }} - {{ "licensePaidFeaturesHelp" | i18n }} + {{ "licensePaidFeaturesHelp" | i18n }}
@@ -84,7 +84,7 @@ rel="noreferrer" > - {{ "billingSyncHelp" | i18n }} + {{ "billingSyncHelp" | i18n }} diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts index 3a671491ffa..1addf426293 100644 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts @@ -55,8 +55,9 @@ export class AdjustPaymentDialogComponent { } submit = async () => { - if (!this.taxInfoComponent.taxFormGroup.valid) { + if (!this.taxInfoComponent?.taxFormGroup.valid && this.taxInfoComponent?.taxFormGroup.touched) { this.taxInfoComponent.taxFormGroup.markAllAsTouched(); + return; } const request = new PaymentRequest(); const response = this.paymentComponent.createPaymentToken().then((result) => { diff --git a/apps/web/src/app/billing/shared/tax-info.component.ts b/apps/web/src/app/billing/shared/tax-info.component.ts index 3f9bb23130d..289c4906e93 100644 --- a/apps/web/src/app/billing/shared/tax-info.component.ts +++ b/apps/web/src/app/billing/shared/tax-info.component.ts @@ -315,17 +315,6 @@ export class TaxInfoComponent { ]; taxRates: TaxRateResponse[]; - // private pristine: TaxInfoView = { - // taxId: null, - // line1: null, - // line2: null, - // city: null, - // state: null, - // postalCode: null, - // country: "US", - // includeTaxId: false, - // }; - constructor( private apiService: ApiService, private route: ActivatedRoute, @@ -435,8 +424,6 @@ export class TaxInfoComponent { try { const taxInfo = await this.apiService.getTaxInfo(); if (taxInfo) { - // this.taxInfo.postalCode = taxInfo.postalCode; - // this.taxInfo.country = taxInfo.country || "US"; this.postalCode = taxInfo.postalCode; this.country = taxInfo.country || "US"; } @@ -451,8 +438,6 @@ export class TaxInfoComponent { this.taxFormGroup.get("postalCode").updateValueAndValidity(); } - //this.pristine = Object.assign({}, this.taxInfo); - // If not the default (US) then trigger onCountryChanged if (this.country !== "US") { this.onCountryChanged.emit(); } @@ -487,7 +472,6 @@ export class TaxInfoComponent { get taxRate() { if (this.taxRates != null) { const localTaxRate = this.taxRates.find( - //(x) => x.country === this.taxInfo.country && x.postalCode === this.taxInfo.postalCode, (x) => x.country === this.country && x.postalCode === this.postalCode, ); return localTaxRate?.rate ?? null; @@ -578,16 +562,6 @@ export class TaxInfoComponent { return this.taxSupportedCountryCodes.includes(countryCode); } - // private hasChanged(): boolean { - // for (const key in this.taxInfo) { - // // eslint-disable-next-line - // if (this.pristine.hasOwnProperty(key) && this.pristine[key] !== this.taxInfo[key]) { - // return true; - // } - // } - // return false; - // } - private taxSupportedCountryCodes: string[] = [ "CN", "FR", diff --git a/apps/web/src/app/billing/shared/update-license-dialog.component.html b/apps/web/src/app/billing/shared/update-license-dialog.component.html index 6430c47528f..7535fe9b30b 100644 --- a/apps/web/src/app/billing/shared/update-license-dialog.component.html +++ b/apps/web/src/app/billing/shared/update-license-dialog.component.html @@ -16,6 +16,7 @@ formControlName="file" (change)="setSelectedFile($event)" hidden + class="tw-hidden" /> {{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }} diff --git a/apps/web/src/app/billing/shared/update-license.component.html b/apps/web/src/app/billing/shared/update-license.component.html index 3cdc6fa3aeb..938179469e4 100644 --- a/apps/web/src/app/billing/shared/update-license.component.html +++ b/apps/web/src/app/billing/shared/update-license.component.html @@ -14,6 +14,7 @@ formControlName="file" (change)="setSelectedFile($event)" hidden + class="tw-hidden" /> {{ "licenseFileDesc" diff --git a/apps/web/src/app/layouts/header/web-header.component.html b/apps/web/src/app/layouts/header/web-header.component.html index e2b3e7910ab..5b55eede778 100644 --- a/apps/web/src/app/layouts/header/web-header.component.html +++ b/apps/web/src/app/layouts/header/web-header.component.html @@ -1,31 +1,3 @@ - - {{ unassignedItemsBannerService.bannerText$ | async | i18n }} - {{ "unassignedItemsBannerCTAPartOne" | i18n }} - {{ "adminConsole" | i18n }} - {{ "unassignedItemsBannerCTAPartTwo" | i18n }} - {{ "learnMore" | i18n }} -
; protected selfHosted: boolean; protected hostname = location.hostname; - protected unassignedItemsBannerEnabled$ = this.configService.getFeatureFlag$( - FeatureFlag.UnassignedItemsBanner, - ); constructor( private route: ActivatedRoute, private platformUtilsService: PlatformUtilsService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private messagingService: MessagingService, - protected unassignedItemsBannerService: UnassignedItemsBannerService, - private configService: ConfigService, private accountService: AccountService, ) { this.routeData$ = this.route.data.pipe( diff --git a/apps/web/src/app/tools/generator.component.html b/apps/web/src/app/tools/generator.component.html index 4be83c3edb5..f52d1f020d3 100644 --- a/apps/web/src/app/tools/generator.component.html +++ b/apps/web/src/app/tools/generator.component.html @@ -126,7 +126,7 @@ [value]="passwordOptions.length" /> (); organization: Organization; organizations: Organization[]; organizations$: Observable; @@ -104,6 +106,7 @@ export class CipherReportComponent implements OnDestroy { } else { this.ciphers = this.ciphers.filter((c: any) => c.orgFilterStatus === status); } + this.dataSource.data = this.ciphers; } async load() { @@ -193,6 +196,8 @@ export class CipherReportComponent implements OnDestroy { return ciph; }); + this.dataSource.data = this.ciphers; + if (this.filterStatus.length > 2) { this.showFilterToggle = true; this.vaultMsg = "vaults"; diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html index 30801a42fdc..fd9fc71c07f 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html @@ -6,13 +6,13 @@ {{ "checkExposedPasswords" | i18n }}
- + {{ "noExposedPasswords" | i18n }} - + - + {{ "exposedPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} - + -
- + + - - - + + + - - -
{{ "name" | i18n }} {{ "owner" | i18n }}
- + - + + {{ c.name }}{{ r.name }} - {{ c.name }} + {{ r.name }} - + - {{ "shared" | i18n }} + {{ "shared" | i18n }} - + - {{ "attachments" | i18n }} + {{ "attachments" | i18n }}
- {{ c.subTitle }} + {{ r.subTitle }}
- {{ "exposedXTimes" | i18n: (exposedPasswordMap.get(c.id) | number) }} + {{ "exposedXTimes" | i18n: (exposedPasswordMap.get(r.id) | number) }}
+ +
diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts index cabc7bdfa12..8503174a937 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts @@ -11,7 +11,6 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; import { CipherReportComponent } from "./cipher-report.component"; - @Component({ selector: "app-exposed-passwords-report", templateUrl: "exposed-passwords-report.component.html", diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html index ae03a3bcb80..ef003287d1f 100644 --- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html +++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html @@ -8,16 +8,16 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}
- + {{ "noInactive2fa" | i18n }} - + - + {{ "inactive2faFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} - + - - + + - + - - - + + + - - - - -
{{ "name" | i18n }} {{ "owner" | i18n }}
- - - {{ - c.name - }} - - +
+ + + {{ r.name }} + + + {{ "shared" | i18n }} + + + + {{ "attachments" | i18n }} + +
+ {{ r.subTitle }} +
+ - - - - - {{ "instructions" | i18n }} -
+ > + + + + + {{ "instructions" | i18n }} + + + +
diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html index 549773ba8ce..b6cd8d06f21 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html @@ -8,16 +8,16 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}
- + {{ "noReusedPasswords" | i18n }} - + - + {{ "reusedPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} - + - - + + - - - + + + - - -
{{ "name" | i18n }} {{ "owner" | i18n }}
- + - + + {{ c.name }}{{ r.name }} - {{ c.name }} + {{ r.name }} - + - {{ "shared" | i18n }} + {{ "shared" | i18n }} - + - {{ "attachments" | i18n }} + {{ "attachments" | i18n }}
- {{ c.subTitle }} + {{ r.subTitle }}
- {{ "reusedXTimes" | i18n: passwordUseMap.get(c.login.password) }} + {{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }}
+ +
diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.html b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.html index ced0ff9731d..87447e45273 100644 --- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.html +++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.html @@ -8,7 +8,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}
@@ -59,7 +59,7 @@ title="{{ 'shared' | i18n }}" aria-hidden="true" > - {{ "shared" | i18n }} + {{ "shared" | i18n }} - {{ "attachments" | i18n }} + {{ "attachments" | i18n }}
{{ c.subTitle }} diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html index a943c8c29ec..3b3fdfa589f 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html @@ -8,16 +8,16 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}
- + {{ "noWeakPasswords" | i18n }} - + - + {{ "weakPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} - + - - + + - - - + + + - -
{{ "name" | i18n }} {{ "owner" | i18n }}
- + - + {{ c.name }}{{ r.name }} - {{ c.name }} + {{ r.name }} - + - {{ "shared" | i18n }} + {{ "shared" | i18n }} - + - {{ "attachments" | i18n }} + {{ "attachments" | i18n }}
- {{ c.subTitle }} + {{ r.subTitle }}
- - {{ passwordStrengthMap.get(c.id)[0] | i18n }} + + {{ passwordStrengthMap.get(r.id)[0] | i18n }}
+ +
diff --git a/apps/web/src/app/tools/send/add-edit.component.html b/apps/web/src/app/tools/send/add-edit.component.html index cc96908eaa9..500100afd49 100644 --- a/apps/web/src/app/tools/send/add-edit.component.html +++ b/apps/web/src/app/tools/send/add-edit.component.html @@ -76,12 +76,13 @@ {{ "sendFileDesc" | i18n }} {{ "maxFileSize" | i18n }} diff --git a/apps/web/src/app/tools/send/send.component.html b/apps/web/src/app/tools/send/send.component.html index 9a66c85c78f..5a88f45cdc6 100644 --- a/apps/web/src/app/tools/send/send.component.html +++ b/apps/web/src/app/tools/send/send.component.html @@ -7,7 +7,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}
@@ -108,7 +108,7 @@ title="{{ 'disabled' | i18n }}" aria-hidden="true" > - {{ "disabled" | i18n }} + {{ "disabled" | i18n }} - {{ "password" | i18n }} + {{ "password" | i18n }} - {{ "maxAccessCountReached" | i18n }} + {{ "maxAccessCountReached" | i18n }} - {{ "expired" | i18n }} + {{ "expired" | i18n }} - {{ "pendingDeletion" | i18n }} + {{ "pendingDeletion" | i18n }} @@ -189,7 +189,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }} diff --git a/apps/web/src/app/vault/components/assign-collections/assign-collections-web.component.html b/apps/web/src/app/vault/components/assign-collections/assign-collections-web.component.html new file mode 100644 index 00000000000..4f5b6234ad9 --- /dev/null +++ b/apps/web/src/app/vault/components/assign-collections/assign-collections-web.component.html @@ -0,0 +1,33 @@ + + + {{ "assignToCollections" | i18n }} + + {{ editableItemCount | pluralize: ("item" | i18n) : ("items" | i18n) }} + + + +
+ +
+ + + + + +
diff --git a/apps/web/src/app/vault/components/assign-collections/assign-collections-web.component.ts b/apps/web/src/app/vault/components/assign-collections/assign-collections-web.component.ts new file mode 100644 index 00000000000..4bbbda94a09 --- /dev/null +++ b/apps/web/src/app/vault/components/assign-collections/assign-collections-web.component.ts @@ -0,0 +1,37 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; + +import { PluralizePipe } from "@bitwarden/angular/pipes/pluralize.pipe"; +import { DialogService } from "@bitwarden/components"; +import { + AssignCollectionsComponent, + CollectionAssignmentParams, + CollectionAssignmentResult, +} from "@bitwarden/vault"; + +import { SharedModule } from "../../../shared"; + +@Component({ + imports: [SharedModule, AssignCollectionsComponent, PluralizePipe], + templateUrl: "./assign-collections-web.component.html", + standalone: true, +}) +export class AssignCollectionsWebComponent { + protected editableItemCount: number; + + constructor( + @Inject(DIALOG_DATA) public params: CollectionAssignmentParams, + private dialogRef: DialogRef, + ) {} + + protected async onCollectionAssign(result: CollectionAssignmentResult) { + this.dialogRef.close(result); + } + + static open(dialogService: DialogService, config: DialogConfig) { + return dialogService.open( + AssignCollectionsWebComponent, + config, + ); + } +} diff --git a/apps/web/src/app/vault/components/assign-collections/index.ts b/apps/web/src/app/vault/components/assign-collections/index.ts new file mode 100644 index 00000000000..0c20f958850 --- /dev/null +++ b/apps/web/src/app/vault/components/assign-collections/index.ts @@ -0,0 +1 @@ +export * from "./assign-collections-web.component"; diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html index af2a8443edf..0e515a307c6 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html @@ -34,7 +34,7 @@ title="{{ 'attachments' | i18n }}" aria-hidden="true" > - {{ "attachments" | i18n }} + {{ "attachments" | i18n }} - {{ "attachmentsNeedFix" | i18n }} + {{ "attachmentsNeedFix" | i18n }}
@@ -69,16 +69,16 @@ - + - + + - + @@ -138,7 +154,12 @@ {{ "restore" | i18n }} - - @@ -125,6 +140,8 @@ [organizations]="allOrganizations" [collections]="allCollections" [checked]="selection.isSelected(item)" + [canEditCipher]="canEditCipher(item.cipher) && vaultBulkManagementActionEnabled" + [vaultBulkManagementActionEnabled]="vaultBulkManagementActionEnabled" (checkedToggled)="selection.toggle(item)" (onEvent)="event($event)" > diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index baca403f181..bfb30f3f769 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -48,6 +48,7 @@ export class VaultItemsComponent { @Input() addAccessStatus: number; @Input() addAccessToggle: boolean; @Input() restrictProviderAccess: boolean; + @Input() vaultBulkManagementActionEnabled = false; private _ciphers?: CipherView[] = []; @Input() get ciphers(): CipherView[] { @@ -93,10 +94,24 @@ export class VaultItemsComponent { ); } + get disableMenu() { + return ( + this.vaultBulkManagementActionEnabled && + !this.bulkMoveAllowed && + !this.showAssignToCollections() && + !this.showDelete() + ); + } + get bulkAssignToCollectionsAllowed() { return this.showBulkAddToCollections && this.ciphers.length > 0; } + // Use new bulk management delete if vaultBulkManagementActionEnabled feature flag is enabled + get deleteAllowed() { + return this.vaultBulkManagementActionEnabled ? this.showDelete() : true; + } + protected canEditCollection(collection: CollectionView): boolean { // Only allow allow deletion if collection editing is enabled and not deleting "Unassigned" if (collection.id === Unassigned) { @@ -192,6 +207,22 @@ export class VaultItemsComponent { return false; } + protected canEditCipher(cipher: CipherView) { + if (cipher.organizationId == null) { + return true; + } + + const organization = this.allOrganizations.find((o) => o.id === cipher.organizationId); + return ( + (organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) && + this.viewingOrgVault) || + cipher.edit + ); + } + private refreshItems() { const collections: VaultItem[] = this.collections.map((collection) => ({ collection })); const ciphers: VaultItem[] = this.ciphers.map((cipher) => ({ cipher })); @@ -235,4 +266,89 @@ export class VaultItemsComponent { .map((item) => item.cipher), }); } + + protected showAssignToCollections(): boolean { + if (!this.showBulkMove) { + return false; + } + + if (this.selection.selected.length === 0) { + return true; + } + + const hasPersonalItems = this.hasPersonalItems(); + const uniqueCipherOrgIds = this.getUniqueOrganizationIds(); + + // Return false if items are from different organizations + if (uniqueCipherOrgIds.size > 1) { + return false; + } + + // If all items are personal, return based on personal items + if (uniqueCipherOrgIds.size === 0) { + return hasPersonalItems; + } + + const [orgId] = uniqueCipherOrgIds; + const organization = this.allOrganizations.find((o) => o.id === orgId); + + const canEditOrManageAllCiphers = + organization?.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) && this.viewingOrgVault; + + const collectionNotSelected = + this.selection.selected.filter((item) => item.collection).length === 0; + + return (canEditOrManageAllCiphers || this.allCiphersHaveEditAccess()) && collectionNotSelected; + } + + protected showDelete(): boolean { + if (this.selection.selected.length === 0) { + return true; + } + + const hasPersonalItems = this.hasPersonalItems(); + const uniqueCipherOrgIds = this.getUniqueOrganizationIds(); + const organizations = Array.from(uniqueCipherOrgIds, (orgId) => + this.allOrganizations.find((o) => o.id === orgId), + ); + + const canEditOrManageAllCiphers = + organizations.length > 0 && + organizations.every((org) => + org?.canEditAllCiphers(this.flexibleCollectionsV1Enabled, this.restrictProviderAccess), + ); + + const canDeleteCollections = this.selection.selected + .filter((item) => item.collection) + .every((item) => item.collection && this.canDeleteCollection(item.collection)); + + const userCanDeleteAccess = + (canEditOrManageAllCiphers || this.allCiphersHaveEditAccess()) && canDeleteCollections; + + if ( + userCanDeleteAccess || + (hasPersonalItems && (!uniqueCipherOrgIds.size || userCanDeleteAccess)) + ) { + return true; + } + + return false; + } + + private hasPersonalItems(): boolean { + return this.selection.selected.some(({ cipher }) => cipher?.organizationId === null); + } + + private allCiphersHaveEditAccess(): boolean { + return this.selection.selected + .filter(({ cipher }) => cipher) + .every(({ cipher }) => cipher?.edit); + } + + private getUniqueOrganizationIds(): Set { + return new Set(this.selection.selected.flatMap((i) => i.cipher?.organizationId ?? [])); + } } diff --git a/apps/web/src/app/vault/individual-vault/add-edit-custom-fields.component.html b/apps/web/src/app/vault/individual-vault/add-edit-custom-fields.component.html index dfba89e9620..1c2a75737e0 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit-custom-fields.component.html +++ b/apps/web/src/app/vault/individual-vault/add-edit-custom-fields.component.html @@ -161,7 +161,7 @@
- + {{ "maxFileSize" | i18n }}
diff --git a/apps/web/src/app/vault/individual-vault/attachments.component.ts b/apps/web/src/app/vault/individual-vault/attachments.component.ts index ae4e8fafabe..3bf87ba4e3c 100644 --- a/apps/web/src/app/vault/individual-vault/attachments.component.ts +++ b/apps/web/src/app/vault/individual-vault/attachments.component.ts @@ -18,7 +18,6 @@ import { DialogService } from "@bitwarden/components"; templateUrl: "attachments.component.html", }) export class AttachmentsComponent extends BaseAttachmentsComponent { - viewOnly = false; protected override componentName = "app-vault-attachments"; constructor( diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.html b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.html index 8843bda2f7b..59341a712d5 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.html +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.html @@ -1,15 +1,16 @@ - {{ "moveSelected" | i18n }} + {{ ((vaultBulkManagementActionEnabled$ | async) ? "addToFolder" : "moveSelected") | i18n }}

{{ "moveSelectedItemsDesc" | i18n: cipherIds.length }}

- {{ "folder" | i18n }} - + {{ "selectFolder" | i18n }} + + + +
diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts index cdf45d0669c..252cdc7ac54 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts @@ -3,6 +3,8 @@ import { Component, Inject, OnInit } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { firstValueFrom, Observable } from "rxjs"; +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"; @@ -45,6 +47,10 @@ export class BulkMoveDialogComponent implements OnInit { }); folders$: Observable; + protected vaultBulkManagementActionEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.VaultBulkManagementAction, + ); + constructor( @Inject(DIALOG_DATA) params: BulkMoveDialogParams, private dialogRef: DialogRef, @@ -53,6 +59,7 @@ export class BulkMoveDialogComponent implements OnInit { private i18nService: I18nService, private folderService: FolderService, private formBuilder: FormBuilder, + private configService: ConfigService, ) { this.cipherIds = params.cipherIds ?? []; } diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html index 780614c3303..f0be76018f7 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.html +++ b/apps/web/src/app/vault/individual-vault/vault.component.html @@ -50,8 +50,10 @@ [showBulkTrashOptions]="filter.type === 'trash'" [useEvents]="false" [showAdminActions]="false" + [showBulkAddToCollections]="vaultBulkManagementActionEnabled$ | async" (onEvent)="onVaultItemsEvent($event)" [flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled$ | async" + [vaultBulkManagementActionEnabled]="vaultBulkManagementActionEnabled$ | async" >
(); private refresh$ = new BehaviorSubject(null); @@ -379,9 +384,7 @@ export class VaultComponent implements OnInit, OnDestroy { (o) => o.canCreateNewCollections && !o.isProviderUser, ); - this.showBulkMove = - filter.type !== "trash" && - (filter.organizationId === undefined || filter.organizationId === Unassigned); + this.showBulkMove = filter.type !== "trash"; this.isEmpty = collections?.length === 0 && ciphers?.length === 0; this.performingInitialLoad = false; @@ -428,6 +431,8 @@ export class VaultComponent implements OnInit, OnDestroy { await this.editCollection(event.item, CollectionDialogTabType.Info); } else if (event.type === "viewCollectionAccess") { await this.editCollection(event.item, CollectionDialogTabType.Access); + } else if (event.type === "assignToCollections") { + await this.bulkAssignToCollections(event.items); } } finally { this.processingEvent = false; @@ -492,12 +497,18 @@ export class VaultComponent implements OnInit, OnDestroy { } } + const canEditAttachments = await this.canEditAttachments(cipher); + const vaultBulkManagementActionEnabled = await firstValueFrom( + this.vaultBulkManagementActionEnabled$, + ); + let madeAttachmentChanges = false; const [modal] = await this.modalService.openViewRef( AttachmentsComponent, this.attachmentsModalRef, (comp) => { comp.cipherId = cipher.id; + comp.viewOnly = !canEditAttachments && vaultBulkManagementActionEnabled; comp.onUploadedAttachment .pipe(takeUntil(this.destroy$)) .subscribe(() => (madeAttachmentChanges = true)); @@ -707,6 +718,47 @@ export class VaultComponent implements OnInit, OnDestroy { } } + async bulkAssignToCollections(ciphers: CipherView[]) { + if (!(await this.repromptCipher(ciphers))) { + return; + } + + if (ciphers.length === 0) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("nothingSelected"), + ); + return; + } + + let availableCollections: CollectionView[] = []; + const orgId = + this.activeFilter.organizationId || + ciphers.find((c) => c.organizationId !== null)?.organizationId; + + if (orgId && orgId !== "MyVault") { + const organization = this.allOrganizations.find((o) => o.id === orgId); + availableCollections = this.allCollections.filter( + (c) => c.organizationId === organization.id && !c.readOnly, + ); + } + + const dialog = AssignCollectionsWebComponent.open(this.dialogService, { + data: { + ciphers, + organizationId: orgId as OrganizationId, + availableCollections, + activeCollection: this.activeFilter?.selectedCollectionNode?.node, + }, + }); + + const result = await lastValueFrom(dialog.closed); + if (result === CollectionAssignmentResult.Saved) { + this.refresh(); + } + } + async cloneCipher(cipher: CipherView) { if (cipher.login?.hasFido2Credentials) { const confirmed = await this.dialogService.openSimpleDialog({ @@ -984,6 +1036,17 @@ export class VaultComponent implements OnInit, OnDestroy { this.refresh$.next(); } + private async canEditAttachments(cipher: CipherView) { + if (cipher.organizationId == null || cipher.edit) { + return true; + } + + const flexibleCollectionsV1Enabled = await this.flexibleCollectionsV1Enabled(); + + const organization = this.allOrganizations.find((o) => o.id === cipher.organizationId); + return organization.canEditAllCiphers(flexibleCollectionsV1Enabled, false); + } + private go(queryParams: any = null) { if (queryParams == null) { queryParams = { diff --git a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.html b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.html deleted file mode 100644 index 520e8077880..00000000000 --- a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.html +++ /dev/null @@ -1,66 +0,0 @@ - - - {{ "assignToCollections" | i18n }} - - {{ pluralize(editableItemCount, "item", "items") }} - - - -
-

{{ "bulkCollectionAssignmentDialogDescription" | i18n }}

- -

- {{ "bulkCollectionAssignmentWarning" | i18n: totalItemCount : readonlyItemCount }} -

- -
- - {{ "selectCollectionsToAssign" | i18n }} - - -
- - - - {{ "assignToTheseCollections" | i18n }} - - - - - - - {{ item.labelName }} - - - - - - - - {{ "noCollectionsAssigned" | i18n }} - - - - -
- - - - - -
diff --git a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts deleted file mode 100644 index 8998629b665..00000000000 --- a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; -import { Subject } from "rxjs"; - -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -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 { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; -import { DialogService, SelectItemView } from "@bitwarden/components"; - -import { SharedModule } from "../../../shared"; - -export interface BulkCollectionAssignmentDialogParams { - organizationId: OrganizationId; - - /** - * The ciphers to be assigned to the collections selected in the dialog. - */ - ciphers: CipherView[]; - - /** - * The collections available to assign the ciphers to. - */ - availableCollections: CollectionView[]; - - /** - * The currently filtered collection. Selected by default. If the user deselects it in the dialog then it will be - * removed from the ciphers upon submission. - */ - activeCollection?: CollectionView; -} - -export enum BulkCollectionAssignmentDialogResult { - Saved = "saved", - Canceled = "canceled", -} - -@Component({ - imports: [SharedModule], - selector: "app-bulk-collection-assignment-dialog", - templateUrl: "./bulk-collection-assignment-dialog.component.html", - standalone: true, -}) -export class BulkCollectionAssignmentDialogComponent implements OnDestroy, OnInit { - protected totalItemCount: number; - protected editableItemCount: number; - protected readonlyItemCount: number; - protected availableCollections: SelectItemView[] = []; - protected selectedCollections: SelectItemView[] = []; - - private editableItems: CipherView[] = []; - private destroy$ = new Subject(); - - protected pluralize = (count: number, singular: string, plural: string) => - `${count} ${this.i18nService.t(count === 1 ? singular : plural)}`; - - constructor( - @Inject(DIALOG_DATA) private params: BulkCollectionAssignmentDialogParams, - private dialogRef: DialogRef, - private cipherService: CipherService, - private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private configService: ConfigService, - private organizationService: OrganizationService, - ) {} - - async ngOnInit() { - // If no ciphers are passed in, close the dialog - if (this.params.ciphers == null || this.params.ciphers.length < 1) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected")); - this.dialogRef.close(BulkCollectionAssignmentDialogResult.Canceled); - return; - } - - const v1FCEnabled = await this.configService.getFeatureFlag(FeatureFlag.FlexibleCollectionsV1); - const restrictProviderAccess = await this.configService.getFeatureFlag( - FeatureFlag.RestrictProviderAccess, - ); - const org = await this.organizationService.get(this.params.organizationId); - - if (org.canEditAllCiphers(v1FCEnabled, restrictProviderAccess)) { - this.editableItems = this.params.ciphers; - } else { - this.editableItems = this.params.ciphers.filter((c) => c.edit); - } - - this.editableItemCount = this.editableItems.length; - - // If no ciphers are editable, close the dialog - if (this.editableItemCount == 0) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("missingPermissions")); - this.dialogRef.close(BulkCollectionAssignmentDialogResult.Canceled); - return; - } - - this.totalItemCount = this.params.ciphers.length; - this.readonlyItemCount = this.totalItemCount - this.editableItemCount; - - this.availableCollections = this.params.availableCollections.map((c) => ({ - icon: "bwi-collection", - id: c.id, - labelName: c.name, - listName: c.name, - })); - - // If the active collection is set, select it by default - if (this.params.activeCollection) { - this.selectCollections([ - { - icon: "bwi-collection", - id: this.params.activeCollection.id, - labelName: this.params.activeCollection.name, - listName: this.params.activeCollection.name, - }, - ]); - } - } - - private sortItems = (a: SelectItemView, b: SelectItemView) => - this.i18nService.collator.compare(a.labelName, b.labelName); - - selectCollections(items: SelectItemView[]) { - this.selectedCollections = [...this.selectedCollections, ...items].sort(this.sortItems); - - this.availableCollections = this.availableCollections.filter( - (item) => !items.find((i) => i.id === item.id), - ); - } - - unselectCollection(i: number) { - const removed = this.selectedCollections.splice(i, 1); - this.availableCollections = [...this.availableCollections, ...removed].sort(this.sortItems); - } - - get isValid() { - return this.params.activeCollection != null || this.selectedCollections.length > 0; - } - - submit = async () => { - if (!this.isValid) { - return; - } - - const cipherIds = this.editableItems.map((i) => i.id as CipherId); - - if (this.selectedCollections.length > 0) { - await this.cipherService.bulkUpdateCollectionsWithServer( - this.params.organizationId, - cipherIds, - this.selectedCollections.map((i) => i.id as CollectionId), - false, - ); - } - - if ( - this.params.activeCollection != null && - this.selectedCollections.find((c) => c.id === this.params.activeCollection.id) == null - ) { - await this.cipherService.bulkUpdateCollectionsWithServer( - this.params.organizationId, - cipherIds, - [this.params.activeCollection.id as CollectionId], - true, - ); - } - - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("successfullyAssignedCollections"), - ); - - this.dialogRef.close(BulkCollectionAssignmentDialogResult.Saved); - }; - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - static open( - dialogService: DialogService, - config: DialogConfig, - ) { - return dialogService.open< - BulkCollectionAssignmentDialogResult, - BulkCollectionAssignmentDialogParams - >(BulkCollectionAssignmentDialogComponent, config); - } -} diff --git a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/index.ts b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/index.ts deleted file mode 100644 index 44042e3267a..00000000000 --- a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./bulk-collection-assignment-dialog.component"; diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index 7d148026c1a..881b0948c01 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -140,7 +140,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}
diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 6622882bf86..07d65656d2f 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -59,12 +59,13 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; import { DialogService, Icons, ToastService } from "@bitwarden/components"; -import { PasswordRepromptService } from "@bitwarden/vault"; +import { CollectionAssignmentResult, PasswordRepromptService } from "@bitwarden/vault"; import { GroupService, GroupView } from "../../admin-console/organizations/core"; import { openEntityEventsDialog } from "../../admin-console/organizations/manage/entity-events.component"; import { VaultFilterService } from "../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service"; import { VaultFilter } from "../../vault/individual-vault/vault-filter/shared/models/vault-filter.model"; +import { AssignCollectionsWebComponent } from "../components/assign-collections"; import { CollectionDialogAction, CollectionDialogTabType, @@ -90,10 +91,6 @@ import { getNestedCollectionTree } from "../utils/collection-utils"; import { AddEditComponent } from "./add-edit.component"; import { AttachmentsComponent } from "./attachments.component"; -import { - BulkCollectionAssignmentDialogComponent, - BulkCollectionAssignmentDialogResult, -} from "./bulk-collection-assignment-dialog"; import { BulkCollectionsDialogComponent, BulkCollectionsDialogResult, @@ -1327,7 +1324,7 @@ export class VaultComponent implements OnInit, OnDestroy { ).filter((c) => c.id != Unassigned); } - const dialog = BulkCollectionAssignmentDialogComponent.open(this.dialogService, { + const dialog = AssignCollectionsWebComponent.open(this.dialogService, { data: { ciphers: items, organizationId: this.organization?.id as OrganizationId, @@ -1337,7 +1334,7 @@ export class VaultComponent implements OnInit, OnDestroy { }); const result = await lastValueFrom(dialog.closed); - if (result === BulkCollectionAssignmentDialogResult.Saved) { + if (result === CollectionAssignmentResult.Saved) { this.refresh(); } } diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index b0174d1be29..9809c1cc7f1 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-posadres" }, - "yourVaultIsLocked": { - "message": "U kluis is vergrendel. Verifieer u hoofwagwoord om voort te gaan." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Is u seker u wil voortgaan?" }, "moveSelectedItemsDesc": { - "message": "Kies ’n vouer waarheen u die $COUNT$ gekose item(s) heen wil skuif.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Sleutel" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "U e-pos is bevestig." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "U kan nie uself tot ’n groep toevoeg nie." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Skrap verskaffer" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index b1435e7364e..6974170dd65 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "عنوان البريد الإلكتروني" }, - "yourVaultIsLocked": { - "message": "خزنتك مقفلة. تحقق من كلمة المرور الرئيسية للمتابعة." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "معرف المستخدم الحالي" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 4343b4ba6fc..d49fd446241 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -174,7 +174,7 @@ "description": "This is the folder for uncategorized items" }, "selfOwnershipLabel": { - "message": "You", + "message": "Siz", "description": "Used as a label to indicate that the user is the owner of an item." }, "addFolder": { @@ -406,13 +406,13 @@ "message": "Element" }, "itemDetails": { - "message": "Item details" + "message": "Element detalları" }, "itemName": { - "message": "Item name" + "message": "Element adı" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "\"Yalnız baxma\" icazələrinə sahib kolleksiyaları silə bilməzsiniz: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-poçt ünvanı" }, - "yourVaultIsLocked": { - "message": "Anbarınız kilidlənib. Davam etmək üçün ana parolunuzu doğrulayın." + "yourVaultIsLockedV2": { + "message": "Anbarınız kilidlənib." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Davam etmək istədiyinizə əminsiniz?" }, "moveSelectedItemsDesc": { - "message": "Seçdiyiniz $COUNT$ elementi daşımaq istədiyiniz qovluğu seçin.", + "message": "Seçilmiş $COUNT$ elementi əlavə etmək istədiyiniz qovluğu seçin.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Kimlik doğrulayıcı tətbiqinizlə aşağıdakı QR kodu skan edin və ya açarı daxil edin." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "QR kod yüklənə bilmədi. Yenidən sınayın və ya aşağıdakı açarı istifadə edin." + }, "key": { "message": "Açar" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Hesab e-poçtu doğrulandı" }, + "emailVerifiedV2": { + "message": "E-poçt doğrulandı" + }, "emailVerifiedFailed": { "message": "E-poçtunuz doğrulana bilmir. Yeni bir doğrulama e-poçtu göndərməyə çalışın." }, @@ -7877,7 +7883,7 @@ "message": "Bu kolleksiyalara təyin et" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Elementlərin paylaşılacağı kolleksiyaları seçin. Bir kolleksiyada bir element güncəlləndikdə, bütün kolleksiyalarda əks olunacaq. Elementləri, yalnız bu kolleksiyalara müraciət edə bilən təşkilat üzvləri görə bilər." + "message": "Yalnız bu kolleksiyalara müraciəti olan təşkilat üzvləri bu elementləri görə biləcək." }, "selectCollectionsToAssign": { "message": "Təyin ediləcək kolleksiyaları seçin" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Özünüzü bir qrupa əlavə edə bilməzsiniz." }, - "unassignedItemsBannerSelfHost": { - "message": "Bildiriş: 2 May 2024-cü ildən etibarən təyin edilməmiş təşkilat elementləri artıq cihazlar arasında Bütün Anbarlar görünüşündə görünməyən və yalnız Admin Konsolu vasitəsilə əlçatan olacaq. Bu elementləri görünən etmək üçün Admin Konsolundan bir kolleksiyaya təyin edin." - }, - "unassignedItemsBannerNotice": { - "message": "Bildiriş: Təyin edilməyən təşkilat elementləri artıq Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Bildiriş: 16 May 2024-cü il tarixindən etibarən, təyin edilməyən təşkilat elementləri cihazlar arasında və Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Bu elementləri görünən etmək üçün", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "bir kolleksiyaya təyin edin.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Provayderi sil" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Üzvlərin düzgün kimlik məlumatlarına müraciət etdiklərinə və onların hesabınlarının güvəndə olduğuna əmin olun. Üzv müraciəti və hesab konfiqurasiyalarının CSV-sini əldə etmək üçün bu hesabatı istifadə edin." }, + "memberAccessReportPageDesc": { + "message": "Qruplar, kolleksiyalar və kolleksiya elementləri arasında təşkilat üzvlərinin müraciətini yoxlanışdan keçirin. CSV xaricə köçürməsi, kolleksiya icazələri və hesab konfiqurasiyaları haqqında məlumatlar da daxil olmaqla hər bir üzv üçün detallı məlumat təqdim edir." + }, "higherKDFIterations": { "message": "Daha yüksək KDF iterasiyaları, ana parolunuzu təcavüzkar tərəfindən \"brute force\" hücumuna qarşı qorumağa kömək edir." }, @@ -8522,7 +8514,49 @@ "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." }, "noInvoicesToList": { - "message": "There are no invoices to list", + "message": "Sadalanacaq heç bir faktura yoxdur", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Bildiriş: Bu oyun sonu, client anbar gizliliyi yaxşılaşdırılacaq və provayder üzvləri artıq client anbar elementlərinə birbaşa müraciət edə bilməyəcək. Suallar üçün", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "Bitwarden dəstəyi ilə əlaqə saxlayın.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsorlu" + }, + "licenseAndBillingManagementDesc": { + "message": "Bitwarden bulud serverində güncəlləmələr etdikdən sonra, ən son dəyişiklikləri tətbiq etmək üçün lisenziya faylınızı yükləyin." + }, + "addToFolder": { + "message": "Qovluğa əlavə et" + }, + "selectFolder": { + "message": "Qovluq seç" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ seçilmiş təşkilata birdəfəlik transfer ediləcək. Artıq bu elementlərə sahib olmaya bilməyəcəksiniz.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$, $ORG$ təşkilatına birdəfəlik transfer ediləcək. Artıq bu elementlərə sahib olmaya bilməyəcəksiniz.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index a2f99bfe6a3..ba53bcc52e4 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Адрас электроннай пошты" }, - "yourVaultIsLocked": { - "message": "Ваша сховішча заблакіравана. Увядзіце асноўны пароль для працягу." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Вы сапраўды хочаце працягнуць?" }, "moveSelectedItemsDesc": { - "message": "Выберыце папку ў якую вы хочаце перамясціць выбраныя элементы (колькасць: $COUNT$ шт.).", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Ключ" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Ваша пошта была праверана." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Немагчыма праверыць вашу пошту. Паспрабуйце адправіць новы праверачны ліст." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 963ab8a5588..9d7e581c3f7 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -174,7 +174,7 @@ "description": "This is the folder for uncategorized items" }, "selfOwnershipLabel": { - "message": "You", + "message": "Вие", "description": "Used as a label to indicate that the user is the owner of an item." }, "addFolder": { @@ -406,13 +406,13 @@ "message": "Елемент" }, "itemDetails": { - "message": "Item details" + "message": "Подробности за елемента" }, "itemName": { - "message": "Item name" + "message": "Име на елемента" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Не можете да премахвате колекции с права „Само за преглед“: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -854,8 +854,8 @@ "emailAddress": { "message": "Адрес на електронната поща" }, - "yourVaultIsLocked": { - "message": "Трезорът е заключен — въведете главната си парола, за да продължите." + "yourVaultIsLockedV2": { + "message": "Трезорът Ви е заключен." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Наистина ли искате да продължите?" }, "moveSelectedItemsDesc": { - "message": "Избор на папка, в която да се преместят $COUNT$ избрани записи.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Сканирайте QR-кода по-долу с приложението за удостоверяване, или въведете ключа." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "QR-кодът не може да бъде зареден. Опитайте отново или използвайте ключа по-долу." + }, "key": { "message": "Ключ" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Адресът на електронната ви поща е потвърден." }, + "emailVerifiedV2": { + "message": "Е-пощата е потвърдена" + }, "emailVerifiedFailed": { "message": "Адресът на електронната ви поща не е потвърден. Пробвайте да изпратите ново писмо за потвърждение." }, @@ -7877,7 +7883,7 @@ "message": "Свързване с тези колекции" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Изберете колекциите, с които да бъдат споделени тези елементи. Когато даден елемент бъде променен в една колекция, промяната ще бъде отразена във всички колекции. Само членовете на организацията с достъп до тези колекции ще могат да виждат елементите." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Изберете колекции за свързване" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Не може да добавяте себе си към групи." }, - "unassignedItemsBannerSelfHost": { - "message": "Известие: от 2 май 2024г. неразпределените елементи на организациите вече няма се виждат в изгледа с „Всички трезори“ на различните устройства, а ще бъдат достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." - }, - "unassignedItemsBannerNotice": { - "message": "Известие: неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори на различните устройства, а са достъпни само през Административната конзола." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Известие: след 16 май 2024, неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори на различните устройства, а ще бъдат достъпни само през Административната конзола." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Добавете тези елементи към колекция в", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "за да ги направите видими.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Изтриване на доставчик" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Уверете се, че членовете имат достъп до правилните идентификационни данни, както и че регистрациите им са защитени. Използвайте този доклад, за да получите файл CSV с достъпа на членовете и настройките на регистрациите им." }, + "memberAccessReportPageDesc": { + "message": "Направете проверка на достъпа на членовете до групи, колекции и елементи в колекциите. Изнасянето на на данните като файл CSV предоставя подробна разбивка по членове, включително информация относно правата за колекции и настройките на регистрациите." + }, "higherKDFIterations": { "message": "По-високите стойности за броя на повторения на KDF може да защитят главната Ви парола от атаки тип „груба сила“." }, @@ -8522,7 +8514,49 @@ "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." }, "noInvoicesToList": { - "message": "There are no invoices to list", + "message": "Няма фактури за показване", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Обявление: По-късно този месец поверителността на клиентския трезор ще бъде подобрена и членовете на доставчик вече няма да имат директен достъп до елементите в клиентския трезор. Ако имате въпроси,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "свържете се с поддръжката на Битуорден.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Спонсорирано" + }, + "licenseAndBillingManagementDesc": { + "message": "След като направите промени в облачния сървър на Битуорден, качете файла с лиценза си, за да приложите последните промени." + }, + "addToFolder": { + "message": "Добавяне в папка" + }, + "selectFolder": { + "message": "Изберете папка" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ ще бъдат преместени завинаги в избраната организация. Вече няма да притежавате тези елементи.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ ще бъдат преместени завинаги в $ORG$. Вече няма да притежавате тези елементи.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index bfa7703dcdf..962b70c0839 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "ইমেইল ঠিকানা" }, - "yourVaultIsLocked": { - "message": "আপনার ভল্ট লক করা আছে। চালিয়ে যেতে আপনার মূল পাসওয়ার্ডটি যাচাই করান।" + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index 5789ea5cf74..198150c31de 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email adresa" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index d768fe09632..bba01123c09 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Adreça electrònica" }, - "yourVaultIsLocked": { - "message": "La caixa forta està bloquejada. Verifiqueu la contrasenya mestra per continuar." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Esteu segur que voleu continuar?" }, "moveSelectedItemsDesc": { - "message": "Trieu una carpeta a la que vulgueu desplaçar els $COUNT$ elements seleccionats.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Clau" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "S'ha verificat el vostre correu electrònic." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "No es pot verificar el vostre correu electrònic. Proveu d'enviar un nou correu electrònic de verificació." }, @@ -7877,7 +7883,7 @@ "message": "Assigna a aquestes col·leccions" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Seleccioneu les col·leccions amb les quals es compartiran els elements. Una vegada que un element s'actualitza en una col·lecció, es reflectirà a totes les col·leccions. Només els membres de l'organització amb accés a aquestes col·leccions podran veure els elements." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Seleccioneu les col·leccions per assignar" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "No podeu afegir-vos vosaltres mateix a un grup." }, - "unassignedItemsBannerSelfHost": { - "message": "Avís: el 2 de maig de 2024, els elements de l'organització no assignats deixaran de ser visibles a la vista \"Totes les caixes fortes\" en tots els dispositius i només es podran accedir des de la Consola d'administració. Assigna aquests elements a una col·lecció des de la Consola d'administració per fer-los visibles." - }, - "unassignedItemsBannerNotice": { - "message": "Avís: els elements de l'organització no assignats ja no són visibles a la visualització de Totes les caixes fortes en tots els dispositius i ara només es poden accedir des de la Consola d'administració." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Avís: el 16 de maig de 2024, els elements de l'organització no assignats deixaran de ser visibles a la visualització de Totes les caixes fortes en tots els dispositius i només es podran accedir des de la Consola d'administració." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assigna aquests elements a una col·lecció de", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "per fer-los visibles.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Suprimeix proveïdor" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 9a94d9b0b25..31efa53c824 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-mailová adresa" }, - "yourVaultIsLocked": { - "message": "Váš trezor je uzamčen. Pro pokračování musíte zadat hlavní heslo." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Opravdu chcete pokračovat?" }, "moveSelectedItemsDesc": { - "message": "Vyberte složku, do které chcete přesunout $COUNT$ vybraných položek.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Naskenujte QR kód pomocí Vaší ověřovací aplikace nebo zadejte klíč." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "QR kód nelze načíst. Zkuste to znovu nebo použijte klíč níže." + }, "key": { "message": "Klíč" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Vaše e-mailová adresa byla ověřena" }, + "emailVerifiedV2": { + "message": "E-mail byl ověřen" + }, "emailVerifiedFailed": { "message": "Nelze ověřit Váš e-mail. Zkuste odeslat nový ověřovací e-mail." }, @@ -7877,7 +7883,7 @@ "message": "Přiřadit k těmto kolekcím" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Vyberte kolekce, se kterými budou položky sdíleny. Jakmile bude položka aktualizována v jedné kolekci, bude zobrazena ve všech kolekcích. Jen členové organizace s přístupem k těmto kolekcím budou moci vidět položky." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Vyberte kolekce pro přiřazení" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Do skupiny nemůžete přidat sami sebe." }, - "unassignedItemsBannerSelfHost": { - "message": "Upozornění: Dne 2. května 2024 již nebudou nepřiřazené položky organizace viditelné v zobrazení Všechny trezory ve všech zařízeních a budou přístupné jen prostřednictvím konzoly správce. Přiřaďte tyto položky do kolekce z konzoly pro správce, aby byly viditelné." - }, - "unassignedItemsBannerNotice": { - "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve Vašem zobrazení všech trezorů napříč zařízeními a jsou nyní přístupné pouze v konzoli správce." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Upozornění: 16. květba 2024 již nebudou nepřiřazené položky organizace viditelné ve Vašem zobrazení všech trezorů napříč zařízeními a budou přístupné pouze v konzoli správce." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Přiřadit tyto položky ke kolekci z", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "aby byly viditelné.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Smazat poskytovatele" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ujistěte se, že členové mají přístup k správným údajům a jejich účty jsou bezpečné. Použijte tuto zprávu k získání CSV přístupu členů a konfigurací účtu." }, + "memberAccessReportPageDesc": { + "message": "Audit přístupu členů organizace ke skupinám, kolekcím a položkám kolekcí. Export CSV poskytuje podrobný rozpis pro jednotlivé členy, včetně informací o oprávněních ke kolekcím a konfiguracích účtů." + }, "higherKDFIterations": { "message": "Více iterací KDF Vám může pomoci ochránit hlavní heslo před útočníkem, který by ho vylákal hrubou silou." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "Žádné faktury k zobrazení", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Upozornění: Koncem tohoto měsíce bude vylepšena ochrana osobních údajů v klientském trezoru a členové poskytovatele již nebudou mít přímý přístup k položkám klientského trezoru. V případě dotazů se", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "obraťte na podporu společnosti Bitwarden.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponzorováno" + }, + "licenseAndBillingManagementDesc": { + "message": "Po provedení aktualizací na cloudovém serveru Bitwardenu nahrajte váš licenční soubor pro použití nejnovějších změn." + }, + "addToFolder": { + "message": "Přidat do složky" + }, + "selectFolder": { + "message": "Vybrat složku" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ bude trvale převedeno do vybrané organizace. Tyto položky již nebudete vlastnit.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ bude trvale převedeno do $ORG$. Tyto položky již nebudete vlastnit.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index fe9a3e8d521..6ebf3c97f8f 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 2b4f4f5abfe..1d64000aad4 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-mailadresse" }, - "yourVaultIsLocked": { - "message": "Din boks er låst. Bekræft din hovedadgangskode for at fortsætte." + "yourVaultIsLockedV2": { + "message": "Boksen er låst." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Sikker på, at du vil fortsætte?" }, "moveSelectedItemsDesc": { - "message": "Vælg en mappe, hvortil de(t) $COUNT$ valgte emne(r) ønskes flyttet.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Skan QR-koden nedenfor med godkendelses-appen eller angiv nøglen." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Kunne ikke indlæse QR-kode. Forsøg igen eller brug nøglen nedenfor." + }, "key": { "message": "Nøgle" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Kontoe-mail bekræftet" }, + "emailVerifiedV2": { + "message": "E-mail bekræftet" + }, "emailVerifiedFailed": { "message": "Kan ikke bekræfte din e-mail. Prøv at sende en ny verifikations-email." }, @@ -7877,7 +7883,7 @@ "message": "Tildel til samlinger" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Vælg de samlinger som emnerne vil blive delt med. Når et emne er opdateret i en samling, vil det blive afspejlet i alle samlinger. Kun organisationsmedlemmer med adgang til disse samlinger vil kunne se emnerne." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Vælg samlinger at tildele" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Man kan ikke føje sig selv til en gruppe." }, - "unassignedItemsBannerSelfHost": { - "message": "Bemærk: Pr. 2. maj 2024 vil utildelte organisationsemner ikke længere være synlige i Alle Bokse-visningen på tværs af enheder og vil kun være tilgængelige via Admin-konsollen. Tildel disse emner til en samling via Admin-konsollen for at gøre dem synlige." - }, - "unassignedItemsBannerNotice": { - "message": "Bemærk: Utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Bemærk: Pr. 16. maj 2024 er utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Tildel disse emner til en samling via", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "for at gøre dem synlige.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Slet udbyder" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Sikr, at medlemmerne har adgang til de rigtige legitimationsoplysninger, og at deres konti er sikre. Brug denne rapport til at få en CSV over medlemsadgang og kontoopsætninger." }, + "memberAccessReportPageDesc": { + "message": "Inspicér organisationsmedlemsadgang på tværs af grupper, samlinger og samlingsemner. CSV-eksporten giver en detaljeret opdeling pr. medlem, herunder oplysninger om samlingstilladelser og kontoopsætninger." + }, "higherKDFIterations": { "message": "Højere KDF-iterationer kan hjælpe med at beskytte hovedadgangskoden mod brute force-angreb." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "Ingen fakturaer at vise", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Bemærk: Senere på måneden forbedres klientboksfortroligheden, og udbydermedlemmer vil ikke længere kunne tilgå klientboksemner direkte. For evt. spørgsmål,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponseret" + }, + "licenseAndBillingManagementDesc": { + "message": "Efter at have foretaget opdateringer på Bitwarden cloud-serveren, uploade licensfilen for at anvende de seneste ændringer." + }, + "addToFolder": { + "message": "Føj til mappe" + }, + "selectFolder": { + "message": "Vælg mappe" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ overføres permanent til den valgte organisation. Man vil ikke længere eje disse emner.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ overføres permanent til $ORG$. Man vil ikke længere eje disse emner.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index 37c5d3f6260..808956a4db3 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -174,7 +174,7 @@ "description": "This is the folder for uncategorized items" }, "selfOwnershipLabel": { - "message": "You", + "message": "Du", "description": "Used as a label to indicate that the user is the owner of an item." }, "addFolder": { @@ -406,13 +406,13 @@ "message": "Eintrag" }, "itemDetails": { - "message": "Item details" + "message": "Eintrag-Details" }, "itemName": { - "message": "Item name" + "message": "Eintrags-Name" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Du kannst Sammlungen mit Leseberechtigung nicht entfernen: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-Mail-Adresse" }, - "yourVaultIsLocked": { - "message": "Dein Tresor ist gesperrt. Überprüfe dein Master-Passwort, um fortzufahren." + "yourVaultIsLockedV2": { + "message": "Dein Tresor ist gesperrt." }, "uuid": { "message": "UUID" @@ -982,17 +982,17 @@ "message": "Authenticator App" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Gib einen Code ein, der von einer Authentifizierungs-App wie dem Bitwarden Authenticator generiert wurde.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP security key" + "message": "Yubico OTP-Sicherheitsschlüssel" }, "yubiKeyDesc": { - "message": "Verwenden Sie einen YubiKey um auf Ihr Konto zuzugreifen. Funtioniert mit YubiKey 4, Nano 4, 4C und NEO Geräten." + "message": "Verwende einen YubiKey 4, 5 oder NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Gib einen von Duo Security generierten Code ein.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1003,13 +1003,13 @@ "message": "Benutze einen FIDO U2F-kompatiblen Sicherheitsschlüssel, um auf dein Konto zuzugreifen." }, "u2fTitle": { - "message": "FIDO U2F Sicherheitsschlüssel" + "message": "FIDO U2F-Sicherheitsschlüssel" }, "webAuthnTitle": { - "message": "FIDO2 WebAuthn" + "message": "Passkey" }, "webAuthnDesc": { - "message": "Benutze einen WebAuthn-kompatiblen Sicherheitsschlüssel, um auf dein Konto zuzugreifen." + "message": "Verwende die Biometrie deines Gerätes oder einen FIDO2-kompatiblen Sicherheitsschlüssel." }, "webAuthnMigrated": { "message": "(Von FIDO migriert)" @@ -1018,7 +1018,7 @@ "message": "E-Mail" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Gib einen an deine E-Mail-Adresse gesendeten Code ein." }, "continue": { "message": "Fortsetzen" @@ -1060,7 +1060,7 @@ "message": "Bist du sicher, dass du fortfahren möchtest?" }, "moveSelectedItemsDesc": { - "message": "Wählen Sie einen Ordner aus, in den Sie $COUNT$ ausgewählte(s) Objekt(e) verschieben möchten.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1659,7 +1659,7 @@ "message": "Geben Sie Ihr Master-Passwort ein, um die Zwei-Faktor-Anmeldeeinstellungen zu ändern." }, "twoStepAuthenticatorInstructionPrefix": { - "message": "Download an authenticator app such as" + "message": "Lade eine Authentifizierungs-App herunter, wie z.B." }, "twoStepAuthenticatorInstructionInfix1": { "message": "," @@ -1680,16 +1680,19 @@ } }, "continueToExternalUrlDesc": { - "message": "You are leaving Bitwarden and launching an external website in a new window." + "message": "Du verlässt Bitwarden und öffnest eine externe Website in einem neuen Fenster." }, "twoStepContinueToBitwardenUrlTitle": { "message": "Weiter zu bitwarden.com?" }, "twoStepContinueToBitwardenUrlDesc": { - "message": "Bitwarden Authenticator allows you to store authenticator keys and generate TOTP codes for 2-step verification flows. Learn more on the bitwarden.com website." + "message": "Mit dem Bitwarden Authenticator kannst du Authentifizierungsschlüssel speichern und TOTP-Codes für Zwei-Faktor-Authentifizierungsprozesse generieren. Erfahre mehr auf der bitwarden.com Website." }, "twoStepAuthenticatorScanCodeV2": { - "message": "Scan the QR code below with your authenticator app or enter the key." + "message": "Scanne den QR-Code unten mit deiner Authentifizierungs-App oder gib den Schlüssel ein." + }, + "twoStepAuthenticatorQRCanvasError": { + "message": "QR-Code konnte nicht geladen werden. Versuche es erneut oder verwende den Schlüssel unten." }, "key": { "message": "Schlüssel" @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Deine Konto-E-Mail-Adresse wurde verifiziert" }, + "emailVerifiedV2": { + "message": "E-Mail-Adresse verifiziert" + }, "emailVerifiedFailed": { "message": "Ihre E-Mail kann nicht verifiziert werden. Versuchen Sie eine neue Bestätigungs-E-Mail zu senden." }, @@ -3706,7 +3712,7 @@ } }, "subscriptionSeatMaxReached": { - "message": "You cannot invite more than $COUNT$ members without increasing your subscription seats.", + "message": "Du kannst nicht mehr als $COUNT$ Mitglieder einladen, ohne deine Benutzerplätze zu erhöhen.", "placeholders": { "count": { "content": "$1", @@ -3787,7 +3793,7 @@ "message": "jederzeit." }, "byContinuingYouAgreeToThe": { - "message": "Indem Sie fortfahren, stimmen Sie unseren" + "message": "Indem du fortfährst, stimmst du den" }, "and": { "message": "und" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Du kannst dich nicht selbst zu einer Gruppe hinzufügen." }, - "unassignedItemsBannerSelfHost": { - "message": "Hinweis: Ab dem 2. Mai 2024 werden nicht zugewiesene Organisationseinträge nicht mehr geräteübergreifend in der Ansicht aller Tresore sichtbar sein und sind nur über die Administrator-Konsole zugänglich. Weise diese Elemente einer Sammlung aus der Administrator-Konsole zu, um sie sichtbar zu machen." - }, - "unassignedItemsBannerNotice": { - "message": "Hinweis: Nicht zugewiesene Organisationseinträge sind nicht mehr geräteübergreifend in der Ansicht aller Tresore sichtbar und nun nur über die Administrator-Konsole zugänglich." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Hinweis: Ab dem 16. Mai 2024 werden nicht zugewiesene Organisationseinträge nicht mehr geräteübergreifend in der Ansicht aller Tresore sichtbar sein und nur über die Administrator-Konsole zugänglich." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Weise diese Einträge einer Sammlung aus der", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "zu, um sie sichtbar zu machen.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Anbieter löschen" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Stelle sicher, dass Mitglieder Zugriff auf die richtigen Zugangsdaten haben und ihre Konten sicher sind. Benutze diesen Bericht, um eine CSV-Datei mit Mitgliederzugriffen und Kontokonfigurationen zu erhalten." }, + "memberAccessReportPageDesc": { + "message": "Kontrolliere den Zugriff von Organisationsmitgliedern auf Gruppen, Sammlungen und Sammlungseinträgen. Der CSV-Export bietet eine detaillierte Aufschlüsselung pro Mitglied, einschließlich Informationen über Sammlungsberechtigungen und Kontenkonfigurationen." + }, "higherKDFIterations": { "message": "Höhere KDF-Iterationen können helfen, dein Master-Passwort vor Brute-Force-Attacken durch einen Angreifer zu schützen." }, @@ -8509,20 +8501,62 @@ "message": "Client details" }, "downloadCSV": { - "message": "CSV herunterladen" + "message": "CSV-Datei herunterladen" }, "monthlySubscriptionUserSeatsMessage": { - "message": "Adjustments to your subscription will result in prorated charges to your billing totals on your next billing period. " + "message": "Anpassungen an deinem Abonnement führen zu einer anteiligen Erhöhung deines Rechnungsbetrags im nächsten Abrechnungszeitraum. " }, "annualSubscriptionUserSeatsMessage": { - "message": "Adjustments to your subscription will result in prorated charges on a monthly billing cycle. " + "message": "Anpassungen an deinem Abonnement führen zu einer anteiligen Erhöhung in deinem monatlichen Abrechnungszeitraum. " }, "billingHistoryDescription": { - "message": "Download a CSV to obtain client details for each billing date. Prorated charges are not included in the CSV and may vary from the linked invoice. For the most accurate billing details, refer to your monthly invoices.", + "message": "Lade eine CSV-Datei herunter, um Kundendetails für jedes Rechnungsdatum zu erhalten. Anteilige Kosten sind nicht in der CSV-Datei enthalten und können von der zugehörigen Rechnung abweichen. Detaillierte Angaben zur Rechnungsstellung findest du in deinen monatlichen Abrechnungen.", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." }, "noInvoicesToList": { - "message": "There are no invoices to list", + "message": "Es gibt keine Rechnungen zum Anzeigen", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Hinweis: Später in diesem Monat wird die Kundentresor-Sicherheit verbessert und Mitglieder eines Anbieters haben dann keinen direkten Zugriff mehr auf Einträge des Kundentresors. Bei Fragen", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "kontaktiere den Bitwarden Support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Gesponsert" + }, + "licenseAndBillingManagementDesc": { + "message": "Lade nach der Durchführung von Aktualisierungen im Bitwarden Cloud Server deine Lizenzdatei hoch, um die neuesten Änderungen anzuwenden." + }, + "addToFolder": { + "message": "Zu Ordner hinzufügen" + }, + "selectFolder": { + "message": "Ordner auswählen" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ werden dauerhaft an die ausgewählte Organisation übertragen. Du wirst diese Einträge nicht mehr besitzen.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ werden dauerhaft an $ORG$ übertragen. Du wirst diese Einträge nicht mehr besitzen.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 96c092051a5..6ce73ff9532 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Διεύθυνση Email" }, - "yourVaultIsLocked": { - "message": "Το vault σας είναι κλειδωμένο. Επαληθεύστε τον κύριο κωδικό πρόσβασης για να συνεχίσετε." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Είστε βέβαιοι ότι θέλετε να συνεχίσετε;" }, "moveSelectedItemsDesc": { - "message": "Επιλέξτε ένα φάκελο στον οποίο θέλετε να μετακινήσετε το $COUNT$ επιλεγμένο(α) στοιχεία.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Κλειδί" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Το email σας έχει επαληθευτεί." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Δεν είναι δυνατή η επαλήθευση του email σας. Δοκιμάστε να στείλετε νέο email επαλήθευσης." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index dbbae60cf70..73396c39c16 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -5519,6 +5519,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -6210,6 +6213,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -7883,7 +7889,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8161,23 +8167,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8547,5 +8536,36 @@ }, "licenseAndBillingManagementDesc": { "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "purchasedSeatsRemoved": { + "message": "purchased seats removed" } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index c6ad1dee113..fd5f3b28293 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -2420,7 +2423,7 @@ "message": "Licence file" }, "licenseFileDesc": { - "message": "Your license file will be named something like $FILE_NAME$", + "message": "Your licence file will be named something like $FILE_NAME$", "placeholders": { "file_name": { "content": "$1", @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organisation members with access to these collections will be able to see the items." + "message": "Only organisation members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organisation items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organisation items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organisation items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organisation member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your licence file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organisation. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 3fc204d5d40..e21f70f68ee 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -2420,7 +2423,7 @@ "message": "Licence file" }, "licenseFileDesc": { - "message": "Your license file will be named something like $FILE_NAME$", + "message": "Your licence file will be named something like $FILE_NAME$", "placeholders": { "file_name": { "content": "$1", @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Your email has been verified." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organisation members with access to these collections will be able to see the items." + "message": "Only organisation members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On 2 May 2024, unassigned organisation items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organisation items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On 16 May 2024, unassigned organisation items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organisation member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your licence file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organisation. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index e618e648545..5150b57946d 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Retpoŝta Adreso" }, - "yourVaultIsLocked": { - "message": "Via trezorejo estas ŝlosita. Kontrolu vian ĉefan pasvorton por daŭrigi." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Elektu dosierujon al kiu vi ŝatus movi la elektitajn erojn $COUNT$.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Konigilo" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Via retpoŝto estis kontrolita." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Ne eblas kontroli vian retpoŝton. Provu sendi novan kontrolan retpoŝton." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 098272168a7..97a0dc44f73 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Correo electrónico" }, - "yourVaultIsLocked": { - "message": "Tu caja fuerte está bloqueada. Verifica tu contraseña maestra para continuar." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "¿Está seguro de que desea continuar?" }, "moveSelectedItemsDesc": { - "message": "Selecciona una carpeta a la que quieras mover los $COUNT$ elementos seleccionados.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Clave" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Tu cuenta de correo ha sido verificada." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "No se ha podido verificar tu cuenta de correo electrónico. Prueba a enviar un nuevo correo de verificación." }, @@ -7877,7 +7883,7 @@ "message": "Asignar a estas colecciones" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Seleccionar colecciones para asignar" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index 881e5d62c7b..d166e1747e7 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -174,7 +174,7 @@ "description": "This is the folder for uncategorized items" }, "selfOwnershipLabel": { - "message": "You", + "message": "Sina", "description": "Used as a label to indicate that the user is the owner of an item." }, "addFolder": { @@ -406,10 +406,10 @@ "message": "Kirje" }, "itemDetails": { - "message": "Item details" + "message": "Vaata detaile" }, "itemName": { - "message": "Item name" + "message": "Kirje nimi" }, "cannotRemoveViewOnlyCollections": { "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", @@ -598,16 +598,16 @@ "message": "Juurdepääs" }, "accessLevel": { - "message": "Access level" + "message": "Juurdepääsu aste" }, "accessing": { - "message": "Accessing" + "message": "Hangin juurdepääsu" }, "loggedOut": { "message": "Välja logitud" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "Sa logisid oma kontolt välja." }, "loginExpired": { "message": "Sessioon on aegunud." @@ -631,82 +631,82 @@ "message": "Logi sisse või loo uus konto." }, "loginWithDevice": { - "message": "Log in with device" + "message": "Logi sisse teise seadmega" }, "loginWithDeviceEnabledNote": { - "message": "Log in with device must be set up in the settings of the Bitwarden app. Need another option?" + "message": "Bitwardeni rakenduse seadistuses peab olema konfigureeritud sisselogimine läbi seadme. Soovid teist valikut?" }, "loginWithMasterPassword": { "message": "Logi sisse ülemparooliga" }, "readingPasskeyLoading": { - "message": "Reading passkey..." + "message": "Loen pääsuvõtit..." }, "readingPasskeyLoadingInfo": { - "message": "Keep this window open and follow prompts from your browser." + "message": "Hoia see aken lahti ja järgi brauseri juhiseid." }, "useADifferentLogInMethod": { - "message": "Use a different log in method" + "message": "Kasuta teist logimismeetodit" }, "loginWithPasskey": { - "message": "Log in with passkey" + "message": "Logi sisse pääsuvõtmega" }, "invalidPasskeyPleaseTryAgain": { - "message": "Invalid Passkey. Please try again." + "message": "Vigane pääsuvõti. Palun proovi uuesti." }, "twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn": { - "message": "2FA for passkeys is not supported. Update the app to log in." + "message": "2FA pääsuvõtmed pole toetatud. Uuenda sisse logimiseks rakendust." }, "loginWithPasskeyInfo": { "message": "Use a generated passkey that will automatically log you in without a password. Biometrics, like facial recognition or fingerprint, or another FIDO2 security method will verify your identity." }, "newPasskey": { - "message": "New passkey" + "message": "Uus pääsukood" }, "learnMoreAboutPasswordless": { - "message": "Learn more about passwordless" + "message": "Uuri lähemalt nullparooli kohta" }, "creatingPasskeyLoading": { - "message": "Creating passkey..." + "message": "Loon pääsuvõtit..." }, "creatingPasskeyLoadingInfo": { - "message": "Keep this window open and follow prompts from your browser." + "message": "Hoia see aken lahti ja järgi brauseri juhiseid." }, "errorCreatingPasskey": { - "message": "Error creating passkey" + "message": "Pääsuvõtme loomine ebaõnnestus" }, "errorCreatingPasskeyInfo": { - "message": "There was a problem creating your passkey." + "message": "Tekkis probleem pääsuvõtme loomisel." }, "passkeySuccessfullyCreated": { - "message": "Passkey successfully created!" + "message": "Pääsuvõti edukalt loodud!" }, "customPasskeyNameInfo": { - "message": "Name your passkey to help you identify it." + "message": "Anna oma pääsuvõtmele nimi, et seda hiljem lihtsamini tuvastada." }, "useForVaultEncryption": { - "message": "Use for vault encryption" + "message": "Kasuta hoidla krüpteerimiseks" }, "useForVaultEncryptionInfo": { "message": "Log in and unlock on supported devices without your master password. Follow the prompts from your browser to finalize setup." }, "useForVaultEncryptionErrorReadingPasskey": { - "message": "Error reading passkey. Try again or uncheck this option." + "message": "Ei õnnestunud pääsuvõtit lugeda. Proovi uuesti või tühjendage see valik." }, "encryptionNotSupported": { - "message": "Encryption not supported" + "message": "Krüpteerimine ei ole toetatud" }, "enablePasskeyEncryption": { - "message": "Set up encryption" + "message": "Seadista krüpteerimine" }, "usedForEncryption": { "message": "Used for encryption" }, "loginWithPasskeyEnabled": { - "message": "Log in with passkey turned on" + "message": "Logi sisse pääsuvõti sisse lülitatult" }, "passkeySaved": { - "message": "$NAME$ saved", + "message": "$NAME$ salvestatud", "placeholders": { "name": { "content": "$1", @@ -715,31 +715,31 @@ } }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Pääsuvõti eemaldatud" }, "removePasskey": { - "message": "Remove passkey" + "message": "Eemalda pääsuvõti" }, "removePasskeyInfo": { - "message": "If all passkeys are removed, you will be unable to log into new devices without your master password." + "message": "Kui kõik pääsuvõtmed on eemaldatud, ei saa sa sisse logida uutesse seadmetesse ilma ülemparoolita." }, "passkeyLimitReachedInfo": { - "message": "Passkey limit reached. Remove a passkey to add another." + "message": "Pääsuvõtmete limiit täitus. Eemalda mõni, et uusi lisada." }, "tryAgain": { - "message": "Try again" + "message": "Proovi uuesti" }, "createAccount": { "message": "Konto loomine" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Määra tugev parool" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" + "message": "Lõpeta konto loomine määrates parooli" }, "newAroundHere": { - "message": "New around here?" + "message": "Kas oled uus?" }, "startTrial": { "message": "Alusta prooviperioodi" @@ -748,10 +748,10 @@ "message": "Logi sisse" }, "verifyIdentity": { - "message": "Verify your Identity" + "message": "Kinnitage oma Identiteet" }, "logInInitiated": { - "message": "Log in initiated" + "message": "Sisselogimine käivitatud" }, "submit": { "message": "Kinnita" @@ -787,7 +787,7 @@ "message": "Ülemparooli vihje" }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Kui sa unustad oma parooli, saad saata parooli vihje e-mailile.\n$CURRENT$/$MAXIMUM$ tähepiirang.", "placeholders": { "current": { "content": "$1", @@ -824,7 +824,7 @@ "message": "Vajalik on ülemparooli uuesti sisestamine." }, "masterPasswordMinlength": { - "message": "Master password must be at least $VALUE$ characters long.", + "message": "Ülemparool peab olema vähemalt $VALUE$ märki pikk.", "description": "The Master Password must be at least a specific number of characters long.", "placeholders": { "value": { @@ -849,13 +849,13 @@ "message": "Tekkis ootamatu viga." }, "expirationDateError": { - "message": "Please select an expiration date that is in the future." + "message": "Palun vali kehtivuse lõppemise tähtaeg tulevikust." }, "emailAddress": { "message": "E-posti aadress" }, - "yourVaultIsLocked": { - "message": "Hoidla on lukus. Jätkamiseks sisesta ülemparool." + "yourVaultIsLockedV2": { + "message": "Sinu hoidla on lukus." }, "uuid": { "message": "UUID" @@ -880,7 +880,7 @@ "message": "Vale ülemparool" }, "invalidFilePassword": { - "message": "Invalid file password, please use the password you entered when you created the export file." + "message": "Vale parool, palun kasuta seda parooli mille sisestasid eksport faili loomisel." }, "lockNow": { "message": "Lukusta paroolihoidla" @@ -889,7 +889,7 @@ "message": "Puuduvad kirjed, mida kuvada." }, "noPermissionToViewAllCollectionItems": { - "message": "You do not have permission to view all items in this collection." + "message": "Sul ei ole õigust vaadata kõiki asju selles kogus." }, "noCollectionsInList": { "message": "Puuduvad kollektsioonid, mida kuvada." @@ -901,7 +901,7 @@ "message": "Puuduvad kasutajad, keda kuvada." }, "noMembersInList": { - "message": "There are no members to list." + "message": "Puuduvad kasutajad, keda kuvada." }, "noEventsInList": { "message": "Puuduvad sündmused, mida kuvada." @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Vali kaust, kuhu soovid need $COUNT$ kirjet liigutada.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1493,10 +1493,10 @@ "message": "Vali imporditav fail" }, "chooseFile": { - "message": "Choose File" + "message": "Vali fail" }, "noFileChosen": { - "message": "No file chosen" + "message": "Ühtegi faili pole valitud" }, "orCopyPasteFileContents": { "message": "või kopeeri/kleebi imporditava faili sisu" @@ -1581,26 +1581,26 @@ "message": "Kaheastmeline kinnitamine" }, "twoStepLoginEnforcement": { - "message": "Two-step Login Enforcement" + "message": "Kaheastmelise Logimise Jõustamine" }, "twoStepLoginDesc": { "message": "Kaitse oma kontot, nõudes sisselogimisel lisakinnitust." }, "twoStepLoginTeamsDesc": { - "message": "Enable two-step login for your organization." + "message": "Luba kaheastmeline logimine enda organisatsioonis." }, "twoStepLoginEnterpriseDescStart": { - "message": "Enforce Bitwarden Two-step Login options for members by using the ", + "message": "Jõusta Bitwardeni Kaheastmelise Logimise valikud liikmetele kasutades ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Enforce Bitwarden Two-step Login options for members by using the Two-step Login Policy.'" }, "twoStepLoginPolicy": { - "message": "Two-step Login Policy" + "message": "Kaheastmelise Sisselogimise Reeglid" }, "twoStepLoginOrganizationDuoDesc": { - "message": "To enforce Two-step Login through Duo, use the options below." + "message": "Et jõustada Kaheastmeline Logimine läbi Duo, kasuta allolevaid valikuid." }, "twoStepLoginOrganizationSsoDesc": { - "message": "If you have setup SSO or plan to, Two-step Login may already be enforced through your Identity Provider." + "message": "Kui sa oled seadistanud SSO või plaanid seda, Kaheastmeline Logimine võib juba olla jõustatud läbi sinu Identiteedi Pakkuja." }, "twoStepLoginRecoveryWarning": { "message": "Kaheastmelise kinnitamine aktiveerimine võib luua olukorra, kus sul on võimatu oma Bitwardeni kontosse sisse logida. Näiteks kui kaotad oma nutiseadme. Taastamise kood võimaldab aga kontole ligi pääseda ka olukorras, kus kaheastmelist kinnitamist ei ole võimalik läbi viia. Sellistel juhtudel ei saa ka Bitwardeni klienditugi sinu kontole ligipääsu taastada. Selle tõttu soovitame taastekoodi välja printida ja seda turvalises kohas hoida." @@ -1659,19 +1659,19 @@ "message": "Kaheastmelise kinnitamise seadete muutmiseks pead sisestama ülemparooli." }, "twoStepAuthenticatorInstructionPrefix": { - "message": "Download an authenticator app such as" + "message": "Lae alla autentiteerimise rakendus nagu" }, "twoStepAuthenticatorInstructionInfix1": { "message": "," }, "twoStepAuthenticatorInstructionInfix2": { - "message": "or" + "message": "või" }, "twoStepAuthenticatorInstructionSuffix": { "message": "." }, "continueToExternalUrlTitle": { - "message": "Continue to $URL$?", + "message": "Mine edasi $URL$-i?", "placeholders": { "url": { "content": "$1", @@ -1680,22 +1680,25 @@ } }, "continueToExternalUrlDesc": { - "message": "You are leaving Bitwarden and launching an external website in a new window." + "message": "Oled lahkumas Bitwardenist ja avamas välist veebilehte uues aknas." }, "twoStepContinueToBitwardenUrlTitle": { - "message": "Continue to bitwarden.com?" + "message": "Mine edasi bitwarden.com-i?" }, "twoStepContinueToBitwardenUrlDesc": { - "message": "Bitwarden Authenticator allows you to store authenticator keys and generate TOTP codes for 2-step verification flows. Learn more on the bitwarden.com website." + "message": "Bitwardeni Autentiteerijaga saad sa hoiustada autentiteerimise võtmeid ja luua TOTP koode kaheastmeliseks kinnitamiseks. Uuri lähemalt bitwarden.com veebilehelt." }, "twoStepAuthenticatorScanCodeV2": { - "message": "Scan the QR code below with your authenticator app or enter the key." + "message": "Skänneeri allolev QR-kood oma autentiteerimisrakendusega või sisesta kood." + }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Ei õnnestunud laadida QR-koodi. Proovi uuesti või kasuta allolevat koodi." }, "key": { "message": "Võti" }, "twoStepAuthenticatorEnterCodeV2": { - "message": "Verification code" + "message": "Kinnituskood" }, "twoStepAuthenticatorReaddDesc": { "message": "Kui soovid lisada veel seadmeid, siis all on kuvatud QR kood (ehk võti), mida autentimisrakendusega kasutada saad." @@ -1776,10 +1779,10 @@ "message": "Sisesta oma Bitwardeni rakenduse informatsioon Duo admini paneelist." }, "twoFactorDuoClientId": { - "message": "Client Id" + "message": "Kliendi Id" }, "twoFactorDuoClientSecret": { - "message": "Client Secret" + "message": "Kliendi Saladus" }, "twoFactorDuoApiHostname": { "message": "API hostinimi" @@ -1862,7 +1865,7 @@ "description": "Vault health reports can be used to evaluate the security of your Bitwarden individual or organization vault." }, "orgsReportsDesc": { - "message": "Identify and close security gaps in your organization's accounts by clicking the reports below.", + "message": "Idenfitseeri ja paranda turvaprobleeme oma organisatsiooni kontodes vajutades allolevatele raportidele.", "description": "Vault health reports can be used to evaluate the security of your Bitwarden individual or organization vault." }, "unsecuredWebsitesReport": { @@ -1875,7 +1878,7 @@ "message": "Leiti ebaturvalisi veebilehti" }, "unsecuredWebsitesFoundReportDesc": { - "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "message": "Leidsime sinu hoidla(st/test) $COUNT$ eset ebaturvaliste URI-dega. Peaksid muutma neid, kui su sait lubab, https:// -iks.", "placeholders": { "count": { "content": "$1", @@ -1900,7 +1903,7 @@ "message": "Kaheastmelise kinnituseta kontod" }, "inactive2faFoundReportDesc": { - "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "message": "Me leitsime $COUNT$ veebilehe(veebilehte) sinu $VAULT$ mis ei pruugi olla seadistatud kaheastmelise logimisega (2fa andmebaasi järgi). Et oma kontosid kaitsta, soovitame tungivalt sisse seada kaheastmelise sisselogimise.", "placeholders": { "count": { "content": "$1", @@ -1928,7 +1931,7 @@ "message": "Avastatud on lekkinud paroole" }, "exposedPasswordsFoundReportDesc": { - "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "message": "Leidsime $COUNT$ eseme(eset) sinu $VAULT$ mille parool paljastati hiljutiste andmelekete käigus. Soovitame tungivalt muuta kohe need paroolid.", "placeholders": { "count": { "content": "$1", @@ -1965,7 +1968,7 @@ "message": "Avastatud on nõrgad paroolid" }, "weakPasswordsFoundReportDesc": { - "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", + "message": "Me leidsime $COUNT$ eset sinu $VAULT$ nõrkade paroolidega. Sa peaksid vahetama need tugevamate vastu.", "placeholders": { "count": { "content": "$1", @@ -1990,7 +1993,7 @@ "message": "Leiti korduvalt kasutatud paroole" }, "reusedPasswordsFoundReportDesc": { - "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", + "message": "Me leidsime $COUNT$ parooli, mida on korduvalt kasutatud sinu $VAULT$. Sa peaksid korduvad paroolid välja vahetama ainulaadsete vastu.", "placeholders": { "count": { "content": "$1", @@ -2148,7 +2151,7 @@ } }, "premiumPriceWithFamilyPlan": { - "message": "Go premium for just $PRICE$ /year, or get premium accounts for $FAMILYPLANUSERCOUNT$ users and unlimited family sharing with a ", + "message": "Hangi preemiumi ainult $PRICE$ eest aastas või hangi preemium $FAMILYPLANUSERCOUNT$ kasutajale pluss piiramatu peresisene jagamine ", "placeholders": { "price": { "content": "$1", @@ -2161,7 +2164,7 @@ } }, "bitwardenFamiliesPlan": { - "message": "Bitwarden Families plan." + "message": "Bitwardeni Pereplaaniga." }, "addons": { "message": "Lisad" @@ -2237,7 +2240,7 @@ } }, "paymentChargedWithUnpaidSubscription": { - "message": "Your payment method will be charged for any unpaid subscriptions." + "message": "Sinu maksemeetodit kasutatakse maksmata tellimuste tasumiseks." }, "paymentChargedWithTrial": { "message": "Valitud pakett sisaldab 7 päevast prooviperioodi. Krediitkaardilt ei võeta raha enne, kui prooviperiood läbi saab. Väljatoodud summa debiteeritakse iga $INTERVAL$. Tellimust on võimalik igal ajal tühistada." @@ -2261,7 +2264,7 @@ "message": "Tühista tellimus" }, "subscriptionExpiration": { - "message": "Subscription expiration" + "message": "Tellimuse lõppemine" }, "subscriptionCanceled": { "message": "Tellimus on tühistatud." @@ -2408,7 +2411,7 @@ "message": "Võta klienditoega ühendust" }, "contactSupportShort": { - "message": "Contact Support" + "message": "Võta kasutajatoega ühendust" }, "updatedPaymentMethod": { "message": "Maksemeetod on muudetud." @@ -2626,7 +2629,7 @@ } }, "trialSecretsManagerThankYou": { - "message": "Thanks for signing up for Bitwarden Secrets Manager for $PLAN$!", + "message": "Aitäh soetamast Bitwarden Secrets Manageri $PLAN$ tellimusega!", "placeholders": { "plan": { "content": "$1", @@ -2719,7 +2722,7 @@ "message": "Tahad kindlasti selle grupi kustutada?" }, "deleteMultipleGroupsConfirmation": { - "message": "Are you sure you want to delete the following $QUANTITY$ group(s)?", + "message": "Kas sa oled kindel, et tahad kustudada $QUANTITY$ gruppi?", "placeholders": { "quantity": { "content": "$1", @@ -2737,7 +2740,7 @@ "message": "Kui kasutaja ligipääsu luba on tühistatud, ei saa ta enam origanisatsiooni andmetele ligi. Ligipääsu taastamiseks ava \"Tühistatud\" kaart." }, "removeUserConfirmationKeyConnector": { - "message": "Warning! This user requires Key Connector to manage their encryption. Removing this user from your organization will permanently deactivate their account. This action cannot be undone. Do you want to proceed?" + "message": "Hoiatus! See kasutaja vajab Key Connectorit oma krüpteeringu haldamiseks. Tema eemaldamine organisatsioonist deaktiveerib jäädavalt tema konto. Seda otsust ei saa muuta. Oled kindel?" }, "externalId": { "message": "Väline ID" @@ -2776,7 +2779,7 @@ "message": "Oled kindel, et soovid selle kogumiku kustutada?" }, "editMember": { - "message": "Edit member" + "message": "Muuda liiget" }, "fieldOnTabRequiresAttention": { "message": "A field on the '$TAB$' tab requires your attention.", @@ -2845,7 +2848,7 @@ "message": "Kõik" }, "addAccess": { - "message": "Add Access" + "message": "Anna Juurdepääs" }, "addAccessFilter": { "message": "Add Access Filter" @@ -2884,7 +2887,7 @@ "message": "CLI" }, "bitWebVault": { - "message": "Bitwarden Web vault" + "message": "Bitwarden Web Vault" }, "bitSecretsManager": { "message": "Bitwarden Secrets Manager" @@ -2911,10 +2914,10 @@ "message": "Sisselogimine nurjus vale kaheastmelise kinnituse tõttu." }, "incorrectPassword": { - "message": "Incorrect password" + "message": "Vale parool" }, "incorrectCode": { - "message": "Incorrect code" + "message": "Vale kood" }, "incorrectPin": { "message": "Incorrect PIN" @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "E-posti aadress on kinnitatud." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "E-posti kinnitamine nurjus. Proovi uut kinnituskirja saata." }, @@ -3781,16 +3787,16 @@ "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "Lõpeta tellimus" }, "atAnyTime": { - "message": "at any time." + "message": "iga hetk." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Jätkates nõustud" }, "and": { - "message": "and" + "message": "ja" }, "acceptPolicies": { "message": "Märkeruudu markeerimisel nõustud järgnevaga:" @@ -5505,16 +5511,16 @@ "message": "Kood on saadetud" }, "verificationCode": { - "message": "Verification code" + "message": "Kinnituskood" }, "confirmIdentity": { "message": "Jätkamiseks kinnita oma identiteet." }, "verificationCodeRequired": { - "message": "Verification code is required." + "message": "Kinnituskood on nõutav." }, "invalidVerificationCode": { - "message": "Invalid verification code" + "message": "Vale kinnituskood" }, "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.", @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 0edc6719686..1220a5dc637 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Emaila" }, - "yourVaultIsLocked": { - "message": "Zure kutxa gotorra blokeatuta dago. Egiaztatu zure pasahitz nagusia jarraitzeko." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Aukeratu $COUNT$ hautatutako artikulu mugitu nahi d(it)uzun karpeta.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Gakoa" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Zure emaila egiaztatu da." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Ezin izan da emaila egiaztatu. Saiatu egiaztatzeko email berri bat bidaltzen." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 033e4b6f7e5..3156ac2b889 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "نشانی ایمیل" }, - "yourVaultIsLocked": { - "message": "گاوصندوق شما قفل است. برای ادامه کلمه عبور اصلی خود را وارد کنید." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "آیا اطمینان دارید که می‌خواهید ادامه دهید؟" }, "moveSelectedItemsDesc": { - "message": "پوشه ای را انتخاب کنید که می‌خواهید $COUNT$ مورد انتخاب شده را به آن منتقل کنید.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "کلید" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "ایمیل حساب تأیید شد" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "تأیید ایمیل شما امکان پذیر نیست. سعی کنید یک ایمیل تأیید جدید ارسال کنید." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 9e01af9828d..eb7fcb5ba0b 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -412,7 +412,7 @@ "message": "Kohteen nimi" }, "cannotRemoveViewOnlyCollections": { - "message": "Et voi poistaa kokoelmia Vain katselu -oikeuksilla: $COLLECTIONS$", + "message": "Et voi poistaa kokoelmia, joihin sinulla on vain tarkasteluoikeus: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -854,8 +854,8 @@ "emailAddress": { "message": "Sähköpostiosoite" }, - "yourVaultIsLocked": { - "message": "Holvi on lukittu. Jatka vahvistamalla pääsalasanasi." + "yourVaultIsLockedV2": { + "message": "Holvisi on lukittu" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Haluatko varmasti jatkaa?" }, "moveSelectedItemsDesc": { - "message": "Valitse kansio, johon haluat siirtää $COUNT$ kohdetta.", + "message": "Valitse kansio, johon haluat lisätä $COUNT$ kohteen/kohdetta.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Skannaa alla oleva QR-koodi todennussovelluksellasi tai syötä avain." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "QR-koodin lataus ei onnistunut. Yritä uudelleen tai käytä alla olevaa avainta." + }, "key": { "message": "Avain" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Tilin sähköpostiosoite on vahvistettu" }, + "emailVerifiedV2": { + "message": "Sähköpostiosoite on vahvistettu" + }, "emailVerifiedFailed": { "message": "Sähköpostiosoitettasi ei voitu vahvistaa. Yritä lähettää uusi vahvistussähköposti." }, @@ -3706,7 +3712,7 @@ } }, "subscriptionSeatMaxReached": { - "message": "You cannot invite more than $COUNT$ members without increasing your subscription seats.", + "message": "Voit kutsua enintään $COUNT$ jäsentä korottamatta tilauksesi käyttäjäpaikkojen määrää.", "placeholders": { "count": { "content": "$1", @@ -7877,7 +7883,7 @@ "message": "Määritä seuraaviin kokoelmiin" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Valitse kokoelmat, joihin kohteet sisällytetään. Kun kohdetta muokataan yhdessä kokoelmassa, päivittyy muutos kaikkiin kokoelmiin. Kohteet näkyvät vain niille organisaation jäsenille, joilla on näiden kokoelmien käyttöoikeus." + "message": "Vain näiden kokoelmien käyttöoikeuden omaavat organisaation jäsenet voivat nähdä kohteet." }, "selectCollectionsToAssign": { "message": "Valitse määritettävät kokoelmat" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Et voi lisätä itseäsi ryhmään." }, - "unassignedItemsBannerSelfHost": { - "message": "Huomioi: Alkaen 2. toukokuuta 2024, määrittämättömät organisaatiokohteet eivät enää näy laitteidesi \"Kaikki holvit\" -näkymissä, vaan ne ovat käytettävissä vain Hallintapaneelista." - }, - "unassignedItemsBannerNotice": { - "message": "Huomioi: Määrittämättömät organisaatiokohteet eivät enää näy laitteidesi \"Kaikki holvit\" -näkymissä, vaan ne ovat käytettävissä vain Hallintapaneelista." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Huomioi: Alkaen 16. toukokuuta 2024, määrittämättömät organisaatiokohteet eivät enää näy laitteidesi \"Kaikki holvit\" -näkymissä, vaan ne ovat käytettävissä vain Hallintapaneelista." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Määritä nämä kohteet kokoelmaan", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "ista, jotta ne näkyvät.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Poista toimittaja" }, @@ -8343,10 +8332,10 @@ } }, "viewInfo": { - "message": "Tarkastele tietoja" + "message": "Näytä tiedot" }, "viewAccess": { - "message": "Tarkastele oikeuksia" + "message": "Näytä oikeudet" }, "noCollectionsSelected": { "message": "Et ole valinnut yhtään kokoelmaa." @@ -8415,7 +8404,7 @@ "message": "Näytä salaisuus" }, "noClients": { - "message": "Näytettäviä päätteitä ei ole" + "message": "Näytettäviä asiakkaita ei ole" }, "providerBillingEmailHint": { "message": "Tämä sähköpostiosoite vastaanottaa kaikki tätä toimittajaa koskevat laskut", @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Varmista, että jäsenillä on oikeiden käyttäjätietojen käyttöoikeus ja että heidän tilinsä on suojattu. Tämän raportin avulla saat CSV-tiedoston käyttäjien oikeuksista ja tiliasetuksista." }, + "memberAccessReportPageDesc": { + "message": "Tarkastele organisaation jäsenten käyttöoikeuksia ryhmiin, kokoelmiin ja kokoelmien kohteisiin. CSV-vienti tuottaa yksityiskohtaisen jäsenkohtaisen erittelyn, joka sisältää kokoelmaoikeudet ja tilimääritykset." + }, "higherKDFIterations": { "message": "Korkeampi KDF-toistojen määrä vahvistaa pääsalasanasi suojausta väsytyshyökkäyksien varalta." }, @@ -8512,10 +8504,10 @@ "message": "Lataa CSV" }, "monthlySubscriptionUserSeatsMessage": { - "message": "Adjustments to your subscription will result in prorated charges to your billing totals on your next billing period. " + "message": "Tilausmuutoksista aiheutuvat laskutussumman muutokset huomioidaan suhteutetusti seuraavan laskutuskauden veloituksessa." }, "annualSubscriptionUserSeatsMessage": { - "message": "Adjustments to your subscription will result in prorated charges on a monthly billing cycle. " + "message": "Tilausmuutoksista aiheutuvat laskutussumman muutokset huomioidaan suhteutetusti kuukausilaskutuksen seuraavassa veloituksessa." }, "billingHistoryDescription": { "message": "Lataa CSV-tiedosto nähdäksesi jokaisen laskutuspäivän asiakastiedot. Korjattuja veloituksia ei sisällytetä CSV-teidostoon ja ne saattavat poiketa liitetystä laskusta. Tarkimmat laskutustiedot näet kuukausilaskuistasi.", @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "Näytettäviä laskuja ei ole", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Huomautus: Asiakasholvien yksityisyyttä parannetaan myöhemmin tässä kuussa, jonka jälkeen toimittajan jäsenillä ei ole enää käyttöoikeutta niiden sisältämiin kohteisiin. Lisätietoja saat", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "Bitwardenin asiakaspalvelusta.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsoroitu" + }, + "licenseAndBillingManagementDesc": { + "message": "Ota uusimmat muutokset käyttöön lataamalla lisenssitiedostosi Bitwardenin pilvipalvelimen muutosten jälkeen." + }, + "addToFolder": { + "message": "Lisää kansioon" + }, + "selectFolder": { + "message": "Valitse kansio" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ siirretään pysyvästi valittulle organisaatiolle, etkä enää omista näitä kohteita.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ siirretään pysyvästi organisaatiolle $ORG$, etkä enää omista näitä kohteita.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index f9c46bcc98f..a89ecc6d2e3 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Naka-lock ang vault mo. Beripikahin ang master password mo para tumuloy." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Sigurado ka bang gusto mong tumuloy?" }, "moveSelectedItemsDesc": { - "message": "Piliin ang folder na paglilipatan ng $COUNT$ napiling item.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Na verify ang email ng account" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Hindi ma verify ang iyong email. Subukan ang pagpapadala ng isang bagong email sa pag verify." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 7e816e70f41..ee1c81c74b6 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Adresse électronique" }, - "yourVaultIsLocked": { - "message": "Votre coffre est verrouillé. Vérifiez votre mot de passe principal pour continuer." + "yourVaultIsLockedV2": { + "message": "Votre coffre est verrouillé." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Êtes-vous sûr(e) de vouloir continuer ?" }, "moveSelectedItemsDesc": { - "message": "Choisissez le dossier vers lequel vous souhaitez déplacer les $COUNT$ élément(s) sélectionné(s).", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scannez le code QR ci-dessous avec votre application d'authentification ou saisissez la clé." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Impossible de charger le code QR. Réessayez ou utilisez la clé ci-dessous." + }, "key": { "message": "Clé" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Courriel du compte vérifié" }, + "emailVerifiedV2": { + "message": "Courriel vérifié" + }, "emailVerifiedFailed": { "message": "Impossible de vérifier votre courriel. Essayez en envoyant un nouveau courriel de vérification." }, @@ -7877,7 +7883,7 @@ "message": "Assigner à ces collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Sélectionnez les collections avec lesquelles les éléments seront partagés. Une fois qu'un élément est mis à jour dans une collection, il le sera aussi dans toutes ces collections. Seuls les membres de l'organisation ayant accès à ces collections pourront voir les éléments." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Sélectionnez les collections à assigner" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Vous ne pouvez pas vous ajouter vous-même à un groupe." }, - "unassignedItemsBannerSelfHost": { - "message": "Remarque : le 2 mai 2024, les éléments d'organisation non assignés ne seront plus visibles dans votre vue Tous les coffres sur les appareils et seront uniquement accessibles via la Console Admin. Assignez ces éléments à une collection à partir de la Console Admin pour les rendre visibles." - }, - "unassignedItemsBannerNotice": { - "message": "Remarque : Les éléments d'organisation non assignés ne sont plus visibles dans la vue Tous les coffres sur les appareils et ne sont maintenant accessibles que via la Console Admin." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Remarque : À partir du 16 mai 2024, les éléments d'organisation non assignés ne seront plus visibles dans la vue Tous les coffres sur les appareils et ne seront maintenant accessibles que via la Console Admin." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assigner ces éléments à une collection depuis", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "pour les rendre visibles.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Supprimer le fournisseur" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "S'assurer que les membres ont accès aux bons identifiants et que leurs comptes sont sécurisés. Utilisez ce rapport pour obtenir un CSV d'accès aux membres et des configurations de compte." }, + "memberAccessReportPageDesc": { + "message": "Audite les accès des membres de l'organisation au sein des groupes, des collections et des éléments des collections. L'exportation CSV fournit un rapport détaillé par membre comprenant des informations sur les collections autorisées et les configurations de compte." + }, "higherKDFIterations": { "message": "Des itérations KDF plus élevées peuvent aider à protéger votre mot de passe principal contre la force brute d'un assaillant." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "Il n'y a aucune facture à afficher", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Remarque : Plus tard ce mois-ci, la confidentialité du coffre du client sera améliorée et les membres du fournisseur n'auront plus un accès direct aux éléments du coffre du client. Pour les questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contactez le support Bitwarden.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Parrainé" + }, + "licenseAndBillingManagementDesc": { + "message": "Après avoir fait des mises à jour sur le serveur cloud de Bitwarden, téléversez votre fichier de licence pour appliquer les modifications les plus récentes." + }, + "addToFolder": { + "message": "Ajouter au dossier" + }, + "selectFolder": { + "message": "Sélectionner un dossier" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ seront transférés à l'organisation sélectionnée de façon permanente. Vous ne serez plus propriétaire de ces éléments.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ seront transférés à $ORG$ de façon permanente. Vous ne serez plus propriétaire de ces éléments.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index bec3ddfbae9..a0393577921 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index afaad1a2366..a5ae8083f2b 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "כתובת אימייל" }, - "yourVaultIsLocked": { - "message": "הכספת שלך נעולה. הזן את הסיסמה הראשית שלך כדי להמשיך." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "בחר תיקיה שאליה תרצה להעביר את $COUNT$ הפריט(ים) שבחרת.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "מפתח" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "כתובת האימייל שלך אומתה." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "לא ניתן לאמת את האימייל שלך. נסה לשלוח מייל אימות חדש." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index e48b9184a40..fa4e16d6ea6 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 14b2a995405..8e9ca538308 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Adresa e-pošte" }, - "yourVaultIsLocked": { - "message": "Tvoj trezor je zaključan. Potvrdi glavnu lozinku za nastavak." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Sigurno želiš nastaviti?" }, "moveSelectedItemsDesc": { - "message": "Odaberi mapu u koju želiš premjestiti odabranih $COUNT$ stavke/i.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Ključ" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Adresa e-pošte je provjerena" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Ne možeš potvrditi svoju e-poštu? Pošalji novu poruku." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index 2fe73008f6e..02f64df294c 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email cím" }, - "yourVaultIsLocked": { - "message": "A széf zárolásra került. A folytatáshoz meg kell adni a mesterjelszót." + "yourVaultIsLockedV2": { + "message": "A széf zárolva van." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Biztos folytatni szeretnénk?" }, "moveSelectedItemsDesc": { - "message": "Célmappa kiválasztás $COUNT$ kijelölt elem áthelyezéséhez.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Olvassuk be az alábbi QR kódot a hitelesítő alkalmazással,vagy írjuk be a kulcsot." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Nem sikerült betölteni a QR-kódot. Próbáljuk újra vagy használjuk az alábbi kulcsot." + }, "key": { "message": "Kulcs" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Az email cím megerősítésre került." }, + "emailVerifiedV2": { + "message": "Az email cím ellenőrzésre került." + }, "emailVerifiedFailed": { "message": "Nem sikerült az email cím ellenőrzése. Új ellenőrző email küldése." }, @@ -7877,7 +7883,7 @@ "message": "Hozzárendelés ezen gyűjteményekhez" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Válasszuk ki azokat a gyűjteményeket, amelyekkel az elemek megosztásra kerülnek. Ha egy elem egy gyűjteményben frissítésre kerül, az az összes gyűjteményben megjelenik. Csak az ezekhez a gyűjteményekhez hozzáféréssel rendelkező szervezeti tagok láthatják az elemeket." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Hozzárendelendő gyűjtemények kiválasztása" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Nem adhadjuk magunkat a csoporthoz." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Megjegyzés: A nem hozzárendelt szervezeti elemek már nem láthatók az Összes széf nézetben a különböző eszközökön és csak az Adminisztrátori konzolon keresztül érhetők el." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Megjegyzés: 2024. május 16-tól a nem hozzárendelt szervezeti elemek többé nem lesznek láthatók az Összes széf nézetben a különböző eszközökön és csak az Adminisztrátoir konzolon keresztül lesznek elérhetők." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Rendeljük hozzá ezeket az elemeket a gyűjteményhez", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "a láthatósághoz.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Győződjünk meg arról, hogy a tagok hozzáférnek a megfelelő hitelesítő adatokhoz és fiókjaik biztonságosak. Ezzel a jelentéssel szerezhetjük be a tagok hozzáférését és a fiókkonfigurációkat tartalmazó CSV fájlt." }, + "memberAccessReportPageDesc": { + "message": "A szervezeti tagok hozzáférésének ellenőrzése a csoportok, gyűjtemények és gyűjteményelemek között. A CSV exportálás tagonként részletes lebontást biztosít, beleértve a gyűjtemény engedélyekre és a fiókkonfigurációkra vonatkozó információkat." + }, "higherKDFIterations": { "message": "A magasabb szintű KDF iterációk segíthetnek megvédeni mesterjelszót a támadók erőszakossága ellen." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "Nincsenek listázandó számlák.", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Szponzorált" + }, + "licenseAndBillingManagementDesc": { + "message": "A Bitwarden felhőkiszolgáló frissítése után töltsük fel a licenszfájlt a legutóbbi módosítások alkalmazásához." + }, + "addToFolder": { + "message": "Mappához adás" + }, + "selectFolder": { + "message": "Mappa kiválasztása" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ véglegesen átkerül a kiválasztott szervezethez. A továbbiakban nem leszünk a tulajdonosa ezeknek az elemeknek.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ véglegesen átkerül $ORG$ szervezeti egységbe. A továbbiakban nem leszünk a tulajdonosa ezeknek az elemeknek.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 2a6cbe38099..c6370a50ab2 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Alamat Surel" }, - "yourVaultIsLocked": { - "message": "Brankas Anda terkunci. Verifikasi kata sandi utama Anda untuk melanjutkan." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Pilih folder tempat Anda ingin memindahkan $COUNT$ item yang dipilih.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Kunci" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Email Anda telah diverifikasi." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Tidak dapat memverifikasi email Anda. Coba kirim email verifikasi baru." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index dde0df5a99f..34d9512210e 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Indirizzo email" }, - "yourVaultIsLocked": { - "message": "La tua cassaforte è bloccata. Verifica la tua password principale per continuare." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Sei sicuro di voler continuare?" }, "moveSelectedItemsDesc": { - "message": "Scegli una cartella in cui vuoi spostare i $COUNT$ elementi selezionati.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Chiave" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Email account verificata" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Impossibile verificare la tua email. Prova a inviare una nuova email di verifica." }, @@ -7877,7 +7883,7 @@ "message": "Assegna a queste raccolte" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Seleziona le raccolte con cui questi elementi saranno condivisi. Una volta un elemento è aggiornato in una raccolta, la modifica si rifletterà in tutte le raccolte. Solo i membri dell'organizzazione con accesso a queste raccolte potranno visualizzare gli elementi." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Seleziona le raccolte da assegnare" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Non puoi aggiungerti a un gruppo." }, - "unassignedItemsBannerSelfHost": { - "message": "Avviso: dal 2 maggio 2024, gli elementi dell'organizzazione non assegnati non saranno più visibili nella tua visualizzazione Tutte le Cassaforti su tutti i dispositivi e saranno accessibili solo tramite la Console di amministrazione. Assegna questi elementi ad una raccolta dalla Console di amministrazione per renderli visibili." - }, - "unassignedItemsBannerNotice": { - "message": "Avviso: gli elementi dell'organizzazione non assegnati non sono più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e sono ora accessibili solo tramite la Console di amministrazione." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Avviso: dal 2 maggio 2024, gli elementi dell'organizzazione non assegnati non saranno più visibili nella tua visualizzazione Tutte le Cassaforti su tutti i dispositivi e saranno accessibili solo tramite la Console di amministrazione." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assegna questi elementi ad una raccolta dalla", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "per renderli visibili.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Elimina fornitore" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 5480984ab78..e8ba94d82bd 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "メールアドレス" }, - "yourVaultIsLocked": { - "message": "保管庫がロックされています。開くにはマスターパスワードを入力してください。" + "yourVaultIsLockedV2": { + "message": "保管庫はロックされています。" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "本当に続行しますか?" }, "moveSelectedItemsDesc": { - "message": "$COUNT$個のアイテムを移動したいフォルダーを選択してください。", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "以下の QR コードを認証アプリでスキャンするか、キーを入力してください。" }, + "twoStepAuthenticatorQRCanvasError": { + "message": "QR コードを読み込めませんでした。もう一度試すか、以下のキーを使用してください。" + }, "key": { "message": "キー" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "メールアドレスが確認されました。" }, + "emailVerifiedV2": { + "message": "メールアドレスを認証しました" + }, "emailVerifiedFailed": { "message": "メールアドレスを確認できませんでした。確認メールを再送信してください。" }, @@ -7877,7 +7883,7 @@ "message": "これらのコレクションに割り当てる" }, "bulkCollectionAssignmentDialogDescription": { - "message": "アイテムを共有するコレクションを選択します。1つのコレクションでアイテムが更新されると、すべてのコレクションに反映されます。これらのコレクションにアクセスできる組織メンバーだけがアイテムを見ることができます。" + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "割り当てるコレクションを選択" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "あなた自身をグループに追加することはできません。" }, - "unassignedItemsBannerSelfHost": { - "message": "お知らせ:2024年5月2日に、 割り当てられていない組織アイテムはデバイス間のすべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。 管理コンソールからコレクションにこれらのアイテムを割り当てると、表示できるようになります。" - }, - "unassignedItemsBannerNotice": { - "message": "注意: 割り当てられていない組織アイテムは、デバイス間のすべての保管庫ビューでは表示されなくなり、管理コンソールからのみアクセスできるようになりました。" - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "お知らせ:2024年5月16日に、 割り当てられていない組織アイテムは、デバイス間のすべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。" - }, - "unassignedItemsBannerCTAPartOne": { - "message": "これらのアイテムのコレクションへの割り当てを", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "で実行すると表示できるようになります。", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "プロバイダを削除" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "メンバーが適切な資格情報にアクセスでき、アカウントが安全であることを確認します。 このレポートを使用するとメンバーアクセスとアカウント設定の CSV を取得できます。" }, + "memberAccessReportPageDesc": { + "message": "組織メンバーによるグループ、コレクション、コレクションアイテム間のアクセスを監査します。 CSV エクスポートには、コレクションの権限とアカウント構成に関する情報を含むメンバーごとの詳細な内訳が表示されます。" + }, "higherKDFIterations": { "message": "KDF 反復回数を多くすることで、攻撃者による総当たり攻撃からマスターパスワードを守ることができます。" }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "一覧表示する請求書がありません", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "お知らせ: 今月後半にクライアント保管庫のプライバシーが改善され、プロバイダのメンバーはクライアント保管庫のアイテムに直接アクセスできなくなります。", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "詳細は Bitwarden サポートにお問い合わせください。", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "Bitwarden クラウドサーバーで更新を行った後、ライセンスファイルをアップロードして最新の変更を適用してください。" + }, + "addToFolder": { + "message": "フォルダーに追加" + }, + "selectFolder": { + "message": "フォルダーを選択" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ は選択した組織に恒久的に移行されます。これらのアイテムはあなたの所有ではなくなります。", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ は $ORG$ に恒久的に移行されます。これらのアイテムはあなたの所有ではなくなります。", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index f6df06494b4..cb76f9ad569 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "ელ-ფოსტის მისამართი" }, - "yourVaultIsLocked": { - "message": "თქვენი საცავი ჩაკეტილია. დაადასტურეთ თქვენი მთავარი პაროლი გასაგრძელებლად." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "დარწმუნებული ხართ რომ გინდათ გაგრძელება?" }, "moveSelectedItemsDesc": { - "message": "აირჩიეთ საქაღალდე რომელშიც გსურთ გადაიტანოთ $COUNT$ არჩეული საგნი(ები).", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index bec3ddfbae9..a0393577921 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 999702fdaf5..2b9b8a3806e 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "ಇಮೇಲ್ ವಿಳಾಸ" }, - "yourVaultIsLocked": { - "message": "ನಿಮ್ಮ ವಾಲ್ಟ್ ಲಾಕ್ ಆಗಿದೆ. ಮುಂದುವರೆಯಲು ನಿಮ್ಮ ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ಪರಿಶೀಲಿಸಿ." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "ನೀವು $COUNT$ ಆಯ್ದ ಐಟಂ (ಗಳನ್ನು) ಗೆ ಸರಿಸಲು ಬಯಸುವ ಫೋಲ್ಡರ್ ಆಯ್ಕೆಮಾಡಿ.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "ಕೀ" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "ನಿಮ್ಮ ಇಮೇಲ್ ಅನ್ನು ಪರಿಶೀಲಿಸಲಾಗಿದೆ." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "ನಿಮ್ಮ ಇಮೇಲ್ ಅನ್ನು ಪರಿಶೀಲಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ. ಹೊಸ ಪರಿಶೀಲನೆ ಇಮೇಲ್ ಕಳುಹಿಸಲು ಪ್ರಯತ್ನಿಸಿ." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index 2e8c9d9e6c3..0fb2f23a524 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "이메일 주소" }, - "yourVaultIsLocked": { - "message": "보관함이 잠겨 있습니다. 마스터 비밀번호를 입력하여 계속하세요." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "정말 계속하시겠습니까?" }, "moveSelectedItemsDesc": { - "message": "선택된 $COUNT$ 개의 항목을 옮길 폴더를 선택하십시오.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "키" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "이메일이 확인되었습니다." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "이메일을 인증할 수 없습니다. 새로운 인증을 이메일로 전송하십시오." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index 21169c61beb..e69ebf72e9c 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -174,7 +174,7 @@ "description": "This is the folder for uncategorized items" }, "selfOwnershipLabel": { - "message": "You", + "message": "Tu", "description": "Used as a label to indicate that the user is the owner of an item." }, "addFolder": { @@ -406,13 +406,13 @@ "message": "Vienums" }, "itemDetails": { - "message": "Item details" + "message": "Vienuma dati" }, "itemName": { - "message": "Item name" + "message": "Vienuma nosaukums" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Nevar noņemt krājumus ar tiesībām \"Tikai skatīt\": $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-pasta adrese" }, - "yourVaultIsLocked": { - "message": "Glabātava ir aizslēgta. Nepieciešams norādīt galveno paroli, lai turpinātu." + "yourVaultIsLockedV2": { + "message": "Glabātava ir slēgta." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Vai tiešām vēlaties turpināt?" }, "moveSelectedItemsDesc": { - "message": "Izvēlēties mapi, uz kuru pārvietot atlasīto(s) $COUNT$ vienumu(s).", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Zemāk esošais kvadrātkods jānolasa ar autnetificētāja lietotni vai jāievada atslēga." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Nevarēja ielādēt kvadrātkodu. Jāmēģina vēlreiz vai jāizmanto zemāk esošā atslēga." + }, "key": { "message": "Atslēga" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "E-pasta adrese ir apstiprināta." }, + "emailVerifiedV2": { + "message": "E-pasta adrese ir apliecināta" + }, "emailVerifiedFailed": { "message": "Nevar apstiprināt e-pasta adresi. Var mēģināt sūtīt atkārtotu apstiprinājuma e-pasta ziņojumu." }, @@ -3706,7 +3712,7 @@ } }, "subscriptionSeatMaxReached": { - "message": "You cannot invite more than $COUNT$ members without increasing your subscription seats.", + "message": "Nevar uzaicināt vairāk kā $COUNT$ dalībnieku(s) bez abonementa vietu skaita palielināšanas.", "placeholders": { "count": { "content": "$1", @@ -6804,7 +6810,7 @@ "message": "Automātiska domēnu apstiprināšana" }, "automaticDomainVerificationProcess": { - "message": "Bitwarden mēģinās pārbaudīt domēnu 3 reizes pirmajās 72 stundās. Ja domēnu nevarēs apstiprināt, būs jāpārbauda DNS ieraksts saimniekdatorā un pašrocīgi jāapstiprina. Domēns tiks noņemts no apvienības pēc 7 dienām, ja tas nebūs apstiprināts" + "message": "Bitwarden mēģinās pārbaudīt domēnu 3 reizes pirmajās 72 stundās. Ja domēnu nevarēs apliecināt, būs jāpārbauda DNS ieraksts saimniekdatorā un pašrocīgi tas jāapliecina. Domēns tiks noņemts no apvienības pēc 7 dienām, ja tas nebūs apliecināts" }, "invalidDomainNameMessage": { "message": "Ievadītā vērtība ir nederīga. Piemēram: mansdomens.lv. Apakšdomēniem ir nepieciešams apstiprināt atsevišķus ierakstus." @@ -6822,7 +6828,7 @@ "message": "Domēns ir saglabāts" }, "domainVerified": { - "message": "Domēns ir apstiprināts" + "message": "Domēns ir apliecināts" }, "duplicateDomainError": { "message": "Vienu domēnu nevar pieprasīt divreiz." @@ -7877,7 +7883,7 @@ "message": "Piešķirt šiem krājumiem" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Jāatlasa krājumi, ar kuriem vienumi tiks kopīgoti. Tiklīdz kāds vienums tiks atjaunināts vienā krājumā, tas atspoguļosies visos pārējos. Tikai apvienības dalībnieki ar piekļuvi šiem krājumiem varēs redzēt vienumus." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Atlasīt krājumus, lai piešķirtu" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Sevi nevar pievienot kopai." }, - "unassignedItemsBannerSelfHost": { - "message": "Jāņem vērā: no 2024. gada 2. maija nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" dažādās ierīcēs un būs sasniedzami tikai no pārvaldības konsoles, kur šie vienumi jāpiešķir krājumam, lai padarītu tos redzamus." - }, - "unassignedItemsBannerNotice": { - "message": "Jāņem vērā: nepiešķirti apvienības vienumi vairs nav redzami skatā \"Visas glabātavas\" dažādās ierīcēs un tagad ir pieejami tikai pārvaldības konsolē." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Jāņem vērā: no 2024. gada 16. maija nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" dažādās ierīcēs un būs pieejami tikai pārvaldības konsolē." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Piešķirt šos vienumus krājumam", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ", lai padarītu tos redzamus.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Izdzēst nodrošinātāju" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Nodrošini, ka dalībniekiem ir piekļuve pareizajiem piekļuves datiem un viņu konti ir droši. Šī atskaite ir izmantojama, lai iegūtu CSV ar dalībnieku piekļuvi un kontu konfigurāciju." }, + "memberAccessReportPageDesc": { + "message": "Pārskatīt apvienības dalībnieku piekļuvi dažādām kopām, krājumiem un krājumu vienumiem. CSV izgūšana sniedz izvērstu pārskatu par katru dalībnieku, tajā skaitā informāciju par krājumu atļaujām un konta konfigurāciju." + }, "higherKDFIterations": { "message": "Lielāks KDF atkārtojumu skaits var palīdzēt aizsargāt galveno paroli pārlases uzbrukuma gadījumā." }, @@ -8512,17 +8504,59 @@ "message": "Lejupielādēt CSV" }, "monthlySubscriptionUserSeatsMessage": { - "message": "Adjustments to your subscription will result in prorated charges to your billing totals on your next billing period. " + "message": "Abonementa pielāgojumi izvērtīsies attiecīgās izmaksās kopējā rēķinā nākamajā norēķinu laika posmā. " }, "annualSubscriptionUserSeatsMessage": { - "message": "Adjustments to your subscription will result in prorated charges on a monthly billing cycle. " + "message": "Abonementa pielāgojumi izvērtīsies attiecīgās izmaksās ikmēneša norēķinos. " }, "billingHistoryDescription": { "message": "Lejupielādēt CSV, lai iegūtu informāciju par klientiem katrā norēķinu datumā. Samērīgā sadalījuma maksas netiek iekļautas CSV un var atšķirties no saistītā rēķina. Visatbilstošāko norēķinu informāciju var iegūt ikmēneša rēķinos.", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." }, "noInvoicesToList": { - "message": "There are no invoices to list", + "message": "Nav rēķinu, ko parādīt", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Paziņojums: šajā mēnesī klienta glabātavas privātums tiks uzlabots, un nodrošinātāja dalībniekiem vairs nebūs tiešas piekļuves klienta glabātavas vienumiem. Ar jautājumiem", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "vērsties pie Bitwarden atbalsta.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsorēts" + }, + "licenseAndBillingManagementDesc": { + "message": "Pēc atjauninājumu veikšanas Bitwarden mākoņa serverī jāaugšupielādē sava licences datne, lai pielietotu nesenākās izmaiņas." + }, + "addToFolder": { + "message": "Pievienot mapei" + }, + "selectFolder": { + "message": "Atlasīt mapi" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ tiks neatgriezeniski nodoti atlasītajai apvienībai. Šie vienumi Tev vairs nepiederēs.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ tiks neatgriezeniski nodoti $ORG$. Šie vienumi Tev vairs nepiederēs.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index b9925a52288..ddab9ea2a9f 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "ഇ-മെയിൽ വിലാസം" }, - "yourVaultIsLocked": { - "message": "നിങ്ങളുടെ വാൾട് പൂട്ടിയിരിക്കുന്നു. തുടരുന്നതിന് നിങ്ങളുടെ പ്രാഥമിക പാസ്‌വേഡ് സ്ഥിരീകരിക്കുക." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "$COUNT$ തിരഞ്ഞെടുത്ത ഇനങ്ങൾ നീക്കാൻ ആഗ്രഹിക്കുന്ന ഒരു ഫോൾഡർ തിരഞ്ഞെടുക്കുക).", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "നിങ്ങളുടെ ഇമെയിൽ സ്ഥിരീകരിച്ചു." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "നിങ്ങളുടെ ഇമെയിൽ പരിശോധിച്ചുറപ്പിക്കാനായില്ല. ഒരു പുതിയ സ്ഥിരീകരണ ഇമെയിൽ അയയ്‌ക്കാൻ ശ്രമിക്കുക." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index bec3ddfbae9..a0393577921 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index bec3ddfbae9..a0393577921 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 80251aa3a81..68c70f67682 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-postadresse" }, - "yourVaultIsLocked": { - "message": "Hvelvet ditt er låst. Kontroller hovedpassordet ditt for å fortsette." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Er du sikker på at du vil fortsette?" }, "moveSelectedItemsDesc": { - "message": "Velg en mappe som du ønsker å flytte $COUNT$ valgt(e) gjenstand(er) til.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Nøkkel" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Din E-postadresse har blitt bekreftet." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Klarte ikke å bekrefte E-postadressen din. Prøv å sende en ny bekreftelses-E-post." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index e7ebce67fc8..333d5a6f87f 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 72b4653c1bd..d24ce9e5a3b 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-mailadres" }, - "yourVaultIsLocked": { - "message": "Je kluis is vergrendeld. Voer je hoofdwachtwoord in om door te gaan." + "yourVaultIsLockedV2": { + "message": "Je kluis is vergrendeld." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Weet je zeker dat je wilt doorgaan?" }, "moveSelectedItemsDesc": { - "message": "Kies een map waar je de $COUNT$ geselecteerde item(s) heen wilt verplaatsen.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan de onderstaande QR-code met je authenticator-app of voer de sleutel in." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Kan de QR-code niet laden. Probeer het opnieuw of gebruik de onderstaande sleutel." + }, "key": { "message": "Sleutel" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Je e-mailadres is geverifieerd." }, + "emailVerifiedV2": { + "message": "E-mailadres geverifieerd" + }, "emailVerifiedFailed": { "message": "Je e-mailadres kon niet worden geverifieerd. Probeer een nieuwe e-mail met verificatielink te versturen." }, @@ -7877,7 +7883,7 @@ "message": "Aan deze collecties toewijzen" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Selecteer de collecies om de items mee te delen. Zodra een item in een collectie is bijgewerkt, werkt dat door in alle collecties. Alleen organisatieleden met toegang tot deze collecties kunnen de items zien." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Collecties voor toewijzen selecteren" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Je kunt jezelf niet aan een groep toevoegen." }, - "unassignedItemsBannerSelfHost": { - "message": "Kennisgeving: Vanaf 2 mei 2024 zijn niet-toegewezen organisatie-items op geen enkel apparaat meer zichtbaar in de weergave van alle kluisjes en alleen toegankelijk via de Admin Console. Je kunt deze items in het Admin Console aan een collectie toewijzen om ze zichtbaar te maken." - }, - "unassignedItemsBannerNotice": { - "message": "Let op: Niet-toegewezen organisatie-items zijn niet langer zichtbaar in de weergave van alle kluissen op verschillende apparaten en zijn nu alleen toegankelijk via de Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Let op: Vanaf 16 mei 2024 zijn niet-toegewezen organisatie-items niet langer zichtbaar in de weergave van alle kluissen op verschillende apparaten en alleen toegankelijk via de Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Deze items toewijzen aan een collectie van de", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "om ze zichtbaar te maken.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Provider verwijderen" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Zorg ervoor dat leden toegang hebben tot de juiste inloggegevens en dat hun accounts veilig zijn. Dit CSV-rapport geeft inzicht in ledentoegang en accountconfiguraties." }, + "memberAccessReportPageDesc": { + "message": "Audit de toegang van een organisatielid tot groepen, verzamelingen en verzamelen van items. De CSV-export biedt een gedetailleerde verdeling per lid, inclusief informatie over verzamelrechten en accountconfiguraties." + }, "higherKDFIterations": { "message": "Hogere KDF-iteraties beschermen je hoofdwachtwoord tegen brute-foce-aanvallen." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "Er zijn geen facturen om weer te geven", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Let op: Later deze maand wordt de privacy van kluis verbeterd en hebben leden geen directe toegang meer tot kluisitems van de client. Voor vragen,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact op Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Gesponsord" + }, + "licenseAndBillingManagementDesc": { + "message": "Na het bijwerken in de Bitwarden-cloud-server, upload je je licentiebestand voor het toepassen van de meest recente wijzigingen." + }, + "addToFolder": { + "message": "Aan map toevoegen" + }, + "selectFolder": { + "message": "Map selecteren" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ worden permanent overgedragen aan de geselecteerde organisatie. Je bent niet langer de eigenaar van deze items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ wordt permanent overgedragen aan $ORG$. Je bent niet langer de eigenaar van deze items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index faae698419f..7ac77cec177 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-postadresse" }, - "yourVaultIsLocked": { - "message": "Kvelvet ditt er låst. Stadfesta hovudpassordet ditt for å halda fram." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Velg ei mappe som du vil flytta $COUNT$ markert(e) oppføring(ar) til.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Nykel" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index bec3ddfbae9..a0393577921 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index d942d916f83..f5a39ccce9f 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Adres e-mail" }, - "yourVaultIsLocked": { - "message": "Sejf jest zablokowany. Wpisz hasło główne, aby kontynuować." + "yourVaultIsLockedV2": { + "message": "Twój sejf jest zablokowany." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Czy na pewno chcesz kontynuować?" }, "moveSelectedItemsDesc": { - "message": "Wybierz folder do którego chcesz przenieść zaznaczone elementy.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Zeskanuj poniższy kod QR za pomocą aplikacji uwierzytelniającej lub wprowadź klucz." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Nie można wyczytać kodu QR. Spróbuj ponownie lub użyj poniższego klucza." + }, "key": { "message": "Klucz" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Adres e-mail został zweryfikowany" }, + "emailVerifiedV2": { + "message": "E-mail zweryfikowany" + }, "emailVerifiedFailed": { "message": "Nie możemy zweryfikować Twojego adresu e-mail. Spróbuj ponownie wysłać wiadomość weryfikacyjną." }, @@ -7877,7 +7883,7 @@ "message": "Przypisz do tych kolekcji" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Wybierz kolekcje, z którymi elementy będą udostępniane. Gdy element zostanie zaktualizowany w jednej kolekcji, zostanie to odzwierciedlone we wszystkich kolekcjach. Tylko członkowie organizacji z dostępem do tych kolekcji będą mogli zobaczyć te elementy." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Wybierz kolekcje do przypisania" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Nie możesz dodać siebie do grupy." }, - "unassignedItemsBannerSelfHost": { - "message": "Uwaga: 2 maja 2024 r. nieprzypisane elementy w organizacji nie będą już widoczne w widoku Wszystkie sejfy na urządzeniach i będą dostępne tylko przez Konsolę Administracyjną. Przypisz te elementy do kolekcji z konsoli administracyjnej, aby były one widoczne." - }, - "unassignedItemsBannerNotice": { - "message": "Uwaga: Nieprzypisane elementy organizacji nie są już widoczne w widoku Wszystkie sejfy na urządzeniach i są teraz dostępne tylko przez Konsolę Administracyjną." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Uwaga: 16 maja 2024 r. nieprzypisana elementy w organizacji nie będą już widoczne w widoku Wszystkie sejfy na urządzeniach i będą dostępne tylko przez Konsolę Administracyjną." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Przypisz te elementy do kolekcji z", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ", aby były widoczne.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Usuń dostawcę" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Upewnij się, że członkowie mają dostęp do odpowiednich danych uwierzytelniających, a ich konta są bezpieczne. Użyj tego raportu, aby uzyskać dostęp do CSV z dostępem użytkownika i konfiguracją konta." }, + "memberAccessReportPageDesc": { + "message": "Dostęp członków organizacji audytowej do wszystkich grup, kolekcji i elementów kolekcji. Eksport CSV zapewnia szczegółowy podział na członków, w tym informacje o uprawnieniach do zbierania i konfiguracjach kont." + }, "higherKDFIterations": { "message": "Wyższe wartości iteracji KDF mogą pomóc chronić Twoje hasło główne przed złamaniem przez atakującego." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "Brak faktur do wyświetlenia", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Uwaga: później w tym miesiącu poprawiona zostanie prywatność sejfu klienta, a członkowie dostawcy nie będą już mieli bezpośredniego dostępu do elementów sejfu klienta. Na pytania,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "skontaktuj się z pomocą techniczną Bitwarden.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsorowane" + }, + "licenseAndBillingManagementDesc": { + "message": "Po dokonaniu aktualizacji na serwerze w chmurze Bitwarden, prześlij plik licencyjny, aby zastosować najnowsze zmiany." + }, + "addToFolder": { + "message": "Dodaj do folderu" + }, + "selectFolder": { + "message": "Wybierz folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ zostanie trwale przeniesiony do wybranej organizacji. Nie będziesz już posiadać tych elementów.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ zostanie trwale przeniesiony do $ORG$. Nie będziesz już właścicielem tych elementów.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index e630663835c..ea8ea2db5f4 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Endereço de e-mail" }, - "yourVaultIsLocked": { - "message": "O seu cofre está bloqueado. Verifique a sua senha mestra para continuar." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Tem certeza que deseja continuar?" }, "moveSelectedItemsDesc": { - "message": "Escolha uma pasta para a qual você deseja mover os $COUNT$ itens selecionados.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Chave" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "O seu e-mail foi verificado." }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Não é possível confirmar o seu e-mail. Tente enviar um novo e-mail de verificação." }, @@ -7877,7 +7883,7 @@ "message": "Atribuir a estas coleções" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Selecione as coleções com as quais os itens serão compartilhados. Assim que um item for atualizado em uma coleção, isso será refletido em todas as coleções. Apenas membros da organização com acesso a essas coleções poderão ver os itens." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Selecione as coleções para atribuir" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Você não pode adicionar você mesmo a um grupo." }, - "unassignedItemsBannerSelfHost": { - "message": "Aviso: Em 2 de maio de 2024, itens da organização não estarão mais visíveis em sua visualização de Todos os Cofres entre dispositivos e só serão acessíveis por meio do painel de administração. Atribuir estes itens a uma coleção do Console de Administração para torná-los visíveis." - }, - "unassignedItemsBannerNotice": { - "message": "Aviso: Itens de organização não atribuídos não estão mais visíveis na sua tela Todos os Cofres através dos dispositivos e agora só são acessíveis por meio do Console de Administração." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Aviso: Em 16 de maio, 2024, itens da organização que não foram atribuídos não estarão mais visíveis em sua visualização de Todos os Cofres dos dispositivos e só serão acessíveis por meio do painel de administração." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Atribua estes itens a uma coleção da", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "para torná-los visíveis.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Excluir Provedor" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index e535189a189..af5f1fb4000 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -812,13 +812,13 @@ "message": "Obter a dica da palavra-passe mestra" }, "emailRequired": { - "message": "É necessário o endereço de e-mail." + "message": "O endereço de e-mail é obrigatório." }, "invalidEmail": { "message": "Endereço de e-mail inválido." }, "masterPasswordRequired": { - "message": "É necessária a palavra-passe mestra." + "message": "A palavra-passe mestra é obrigatória." }, "confirmMasterPasswordRequired": { "message": "É necessário reescrever a palavra-passe mestra." @@ -854,8 +854,8 @@ "emailAddress": { "message": "Endereço de e-mail" }, - "yourVaultIsLocked": { - "message": "O seu cofre está bloqueado. Verifique a sua palavra-passe mestra para continuar." + "yourVaultIsLockedV2": { + "message": "O seu cofre está bloqueado" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Tem a certeza de que deseja continuar?" }, "moveSelectedItemsDesc": { - "message": "Escolha uma pasta para a qual pretende mover o(s) $COUNT$ item(ns) selecionado(s).", + "message": "Escolha uma pasta à qual pretende adicionar o(s) $COUNT$ item(ns) selecionado(s).", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Digitalize o código QR abaixo com a sua aplicação de autenticação ou introduza a chave." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Não foi possível carregar o código QR. Tente novamente ou utilize a chave abaixo." + }, "key": { "message": "Chave" }, @@ -2671,7 +2674,7 @@ "message": "Sair" }, "leaveOrganizationConfirmation": { - "message": "Tem a certeza de que pretende deixar esta organização?" + "message": "Tem a certeza de que pretende sair desta organização?" }, "leftOrganization": { "message": "Saiu da organização" @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "E-mail da conta verificado" }, + "emailVerifiedV2": { + "message": "E-mail verificado" + }, "emailVerifiedFailed": { "message": "Não foi possível verificar o seu e-mail. Tente enviar um novo e-mail de verificação." }, @@ -4807,7 +4813,7 @@ "message": "Para verificar a sua 2FA, por favor, clique no botão abaixo." }, "webAuthnAuthenticate": { - "message": "Autenticar WebAuthn" + "message": "Autenticar o WebAuthn" }, "webAuthnNotSupported": { "message": "O WebAuthn não é suportado por este navegador." @@ -5526,7 +5532,7 @@ } }, "leaveOrganization": { - "message": "Deixar a organização" + "message": "Sair da organização" }, "removeMasterPassword": { "message": "Remover palavra-passe mestra" @@ -5736,7 +5742,7 @@ "message": "1 campo acima precisa da sua atenção." }, "fieldRequiredError": { - "message": "$FIELDNAME$ é necessário.", + "message": "$FIELDNAME$ obrigatório.", "placeholders": { "fieldname": { "content": "$1", @@ -5745,7 +5751,7 @@ } }, "required": { - "message": "necessário" + "message": "obrigatório" }, "charactersCurrentAndMaximum": { "message": "$CURRENT$/$MAX$ máximo de caracteres", @@ -6133,7 +6139,7 @@ "description": "the text, 'SCIM', is an acronymn and should not be translated." }, "inputRequired": { - "message": "Campo necessário." + "message": "Campo obrigatório." }, "inputEmail": { "message": "O campo não é um endereço de e-mail." @@ -6667,7 +6673,7 @@ "description": "A unique string that gives a client application (eg. CLI) access to a secret or set of secrets." }, "accessTokenExpirationRequired": { - "message": "Prazo de validade necessário", + "message": "Prazo de validade obrigatório", "description": "Error message indicating that an expiration date for the access token must be set." }, "accessTokenCreatedAndCopied": { @@ -7877,7 +7883,7 @@ "message": "Atribuir a estas coleções" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Selecione as coleções com as quais os itens serão partilhados. Assim que um item for atualizado numa coleção, será refletido em todas as coleções. Apenas os membros da organização com acesso a estas coleções poderão ver os itens." + "message": "Apenas os membros da organização com acesso a estas coleções poderão ver os itens." }, "selectCollectionsToAssign": { "message": "Selecione as coleções a atribuir" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Não se pode adicionar a si próprio a um grupo." }, - "unassignedItemsBannerSelfHost": { - "message": "Aviso: A 2 de maio de 2024, os itens da organização não atribuídos deixarão de estar visíveis na vista Todos os cofres em todos os dispositivos e só estarão acessíveis através da Consola de administração. Atribua estes itens a uma coleção a partir da Consola de administração para os tornar visíveis." - }, - "unassignedItemsBannerNotice": { - "message": "Aviso: Os itens da organização não atribuídos já não são visíveis na vista Todos os cofres em todos os dispositivos e agora só estão acessíveis através da Consola de administração." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Aviso: A 16 de maio de 2024, os itens da organização não atribuídos deixarão de estar visíveis na vista Todos os cofres em todos os dispositivos e só estarão acessíveis através da Consola de administração." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Atribua estes itens a uma coleção a partir da", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "para os tornar visíveis.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Eliminar fornecedor" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Certifique-se de que os membros têm acesso às credenciais corretas e que as suas contas estão seguras. Utilize este relatório para obter um CSV das configurações de acesso e de contas dos membros." }, + "memberAccessReportPageDesc": { + "message": "Audite o acesso dos membros da organização a grupos, coleções e itens de coleção. A exportação CSV fornece uma análise detalhada por membro, incluindo informações sobre permissões de coleção e configurações de conta." + }, "higherKDFIterations": { "message": "Iterações KDF mais altas podem ajudar a proteger a sua palavra-passe mestra de ser forçada por um atacante." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "Não há faturas a enumerar", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Aviso: No final deste mês, a privacidade do cofre do cliente será melhorada e os membros do fornecedor deixarão de ter acesso direto aos itens do cofre do cliente. Para questões,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contacte o suporte do Bitwarden.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Patrocinado" + }, + "licenseAndBillingManagementDesc": { + "message": "Depois de fazer atualizações no servidor de nuvem Bitwarden, carregue o seu ficheiro de licença para aplicar as alterações mais recentes." + }, + "addToFolder": { + "message": "Adicionar à pasta" + }, + "selectFolder": { + "message": "Selecionar pasta" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ serão permanentemente transferidos para a organização selecionada. Estes itens deixarão de lhe pertencer.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ serão permanentemente transferidos para $ORG$. Deixará de ser proprietário destes itens.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index b5da535a0fb..421b3ac2720 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Adresă de e-mail" }, - "yourVaultIsLocked": { - "message": "Seiful dvs. este blocat. Verificați parola principală pentru a continua." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Alegeți un dosar în care doriți să mutați $COUNT$ articole selectate.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Cheie" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "E-mail cont verificat" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "E-mailul dvs. nu a putut fi verificat. Încercați să trimiteți un nou e-mail de verificare." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 4a0dcf3e1f7..33f943c3d0a 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Адрес email" }, - "yourVaultIsLocked": { - "message": "Ваше хранилище заблокировано. Для продолжения введите мастер-пароль." + "yourVaultIsLockedV2": { + "message": "Ваше хранилище заблокировано." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Вы действительно хотите продолжить?" }, "moveSelectedItemsDesc": { - "message": "Выберите папку, в которую вы хотите переместить выбранные элементы ($COUNT$ шт.).", + "message": "Выберите папку, в которую вы хотите добавить выбранные элементы ($COUNT$).", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Сосканируйте приведенный ниже QR-код с помощью приложения-аутентификатора или введите ключ." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Не удалось загрузить QR-код. Попробуйте еще раз или воспользуйтесь ключом ниже." + }, "key": { "message": "Ключ" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Адрес email аккаунта подтвержден" }, + "emailVerifiedV2": { + "message": "Email подтвержден" + }, "emailVerifiedFailed": { "message": "Не удалось подтвердить ваш email. Попробуйте отправить новое письмо с подтверждением." }, @@ -7877,7 +7883,7 @@ "message": "Назначить этим коллекциям" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Выберите коллекции, в которые будут переданы элементы. Если элемент обновлен в одной коллекции, это изменение будет отражено во всех коллекциях. Только члены организации, имеющие доступ к этим коллекциям, смогут видеть элементы." + "message": "Только члены организации, имеющие доступ к этим коллекциям, смогут видеть элементы." }, "selectCollectionsToAssign": { "message": "Выбрать коллекции для назначения" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Нельзя добавить самого себя в группу." }, - "unassignedItemsBannerSelfHost": { - "message": "Уведомление: 2 мая 2024 года неприсвоенные элементы организации больше не будут отображаться в представлении \"Все хранилища\" на всех устройствах и будут доступны только через консоль администратора. Назначьте эти элементы коллекции в консоли администратора, чтобы сделать их видимыми." - }, - "unassignedItemsBannerNotice": { - "message": "Уведомление: Неприсвоенные элементы организации больше не отображаются в представлении \"Все хранилища\" на всех устройствах и теперь доступны только через консоль администратора." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Уведомление: с 16 мая 2024 года неназначенные элементы организации больше не будут отображаться в представлении \"Все хранилища\" на всех устройствах и будут доступны только через консоль администратора." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Назначьте эти элементы в коллекцию из", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "чтобы сделать их видимыми.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Удалить провайдера" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Убедитесь, что пользователи имеют доступ к необходимым им учетным данным, а их аккаунты надежно защищены. Используйте этот отчет, чтобы получить CSV-файл с данными о доступе пользователей и конфигурациях аккаунтов." }, + "memberAccessReportPageDesc": { + "message": "Аудит доступа членов организации к группам, коллекциям и элементам коллекций. Экспорт в формате CSV содержит подробную разбивку по членам, включая информацию о разрешениях на коллекции и конфигурациях учетных записей." + }, "higherKDFIterations": { "message": "Увеличение числа итераций KDF может помочь защитить ваш мастер-пароль от взлома его злоумышленником." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "Нет счетов", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Уведомление: позднее в этом месяце конфиденциальность клиентского хранилища будет улучшена, и провайдеры больше не будут иметь прямого доступа к элементам клиентского хранилища. По вопросам", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "обращайтесь в службу поддержки Bitwarden.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Спонсировано" + }, + "licenseAndBillingManagementDesc": { + "message": "После обновления на сервере Bitwarden загрузите файл лицензии для применения последних изменений." + }, + "addToFolder": { + "message": "Добавить в папку" + }, + "selectFolder": { + "message": "Выбрать папку" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ будут навсегда переданы выбранной организации. Вы больше не будете владельцем этих элементов.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ будут навсегда переданы $ORG$. Вы больше не будете владельцем этих элементов.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index 57ef40607e8..c750bb57545 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "වි-තැපැල් ලිපිනය" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 99fe7fc2962..933014f8726 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Emailová adresa" }, - "yourVaultIsLocked": { - "message": "Váš trezor je uzamknutý. Overte sa hlavným heslom ak chcete pokračovať." + "yourVaultIsLockedV2": { + "message": "Váš trezor je zamknutý." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Ste si istí, že chcete pokračovať?" }, "moveSelectedItemsDesc": { - "message": "Vyberte priečinok do ktorého chcete presunúť $COUNT$ vybraných položiek.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "S vašou overovacou aplikáciou skenujte QR kód nižšie, alebo ručne zadajte kľúč." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Nepodarilo sa načítať QR kód. Skúste to znova, alebo použite kľúč nižšie." + }, "key": { "message": "Kľúč" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Emailová adresa konta bola overená" }, + "emailVerifiedV2": { + "message": "Email bol overený" + }, "emailVerifiedFailed": { "message": "Overovanie zlyhalo. Skúste si odoslať nový verifikačný e-mail." }, @@ -7877,7 +7883,7 @@ "message": "Prideliť k týmto zbierkam" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Vyberte zbierky s ktorými budú položky zdieľané. Zmeny položky v jednej zbierke sa prejavia vo všetkých zbierkach. Iba členovia organizácie s prístupom k týmto zbierkam budu položky vidieť." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Vyberte zbierky na pridelenie" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Upozornenie: 2. mája nepriradené položky organizácie už nebudú viditeľné v zobrazení Všetky Trezory a budú prístupné len cez administrátorskú konzolu. Aby boli viditeľné, priraďte tieto položky do kolekcie z konzoly administrátora." - }, - "unassignedItemsBannerNotice": { - "message": "Upozornenie: Nepriradené položky organizácie už nie sú viditeľné v zobrazení Všetky trezory a sú prístupné iba cez Správcovskú konzolu." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Upozornenie: 16. mája 2024 nepriradené položky organizácie už nebudú viditeľné v zobrazení Všetky trezory a budú prístupné iba cez Správcovskú konzolu." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Priradiť tieto položky do zbierky zo", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ", aby boli viditeľné.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Odstrániť poskytovateľa" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Uistite sa, že členovia majú prístup k správnym prístupovým údajom a ich kontá sú bezpečné. Prostredníctvom tohto reportu získajte CSV o prístupe a konfigurácii členov." }, + "memberAccessReportPageDesc": { + "message": "Audit prístupu členov organizácie v skupinách, kolekciách a položkách kolekcie. Export CSV poskytuje podrobný rozpis podľa jednotlivých členov vrátane informácií o oprávneniach kolekcií a konfiguráciách účtov." + }, "higherKDFIterations": { "message": "Zvýšenie počtu KDF iterácií môže pomôcť chrániť vaše hlavné heslo pri brute force útoku." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "Neexistujú žiadne faktúry na zobrazenie", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Upozornenie: Koncom tohto mesiaca sa zlepší ochrana osobných údajov v trezore klienta a členovia poskytovateľa už nebudú mať priamy prístup k položkám v trezore klienta. V prípade otázok", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "kontaktujte podporu spoločnosti Bitwarden.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponzorované" + }, + "licenseAndBillingManagementDesc": { + "message": "Po vykonaní aktualizácií na cloudovom serveri Bitwarden nahrajte svoj licenčný súbor, aby ste aplikovali najnovšie zmeny." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index 17dc8995a34..ed777b1251d 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-poštni naslov" }, - "yourVaultIsLocked": { - "message": "Vaš trezor je zaklenjen. Potrdite vaše glavno geslo za nadaljevanje." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Ste prepričani, da želite nadaljevati?" }, "moveSelectedItemsDesc": { - "message": "Izberite mapo, v katero bi radi premaknili teh $COUNT$ elementov.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Ključ" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index 626871880bf..c0e487db8e0 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -174,7 +174,7 @@ "description": "This is the folder for uncategorized items" }, "selfOwnershipLabel": { - "message": "You", + "message": "Ти", "description": "Used as a label to indicate that the user is the owner of an item." }, "addFolder": { @@ -406,13 +406,13 @@ "message": "Ставка" }, "itemDetails": { - "message": "Item details" + "message": "Детаљи ставке" }, "itemName": { - "message": "Item name" + "message": "Име ставке" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Не можете уклонити колекције са дозволама само за приказ: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -854,8 +854,8 @@ "emailAddress": { "message": "Имејл" }, - "yourVaultIsLocked": { - "message": "Сеф је блокиран. Унесите главну лозинку за наставак." + "yourVaultIsLockedV2": { + "message": "Ваш сеф је блокиран" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Желите ли заиста да наставите?" }, "moveSelectedItemsDesc": { - "message": "Изаберите фасциклу у коју желите да преместите одабране $COUNT$ ставке.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Скенирајте КР кôд у наставку помоћу апликације за аутентификацију или унесите кључ." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Учитавање QR кôда није успело. Покушајте поново или користите тастер испод." + }, "key": { "message": "Кључ" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Ваш имејл је потврђен." }, + "emailVerifiedV2": { + "message": "Имејл верификован" + }, "emailVerifiedFailed": { "message": "Није могуће верификовати ваш имејл. Покушајте да пошаљете нову поруку за верификацију." }, @@ -7877,7 +7883,7 @@ "message": "Додели овим колекцијама" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Изаберите колекције са којима ће се ставке делити. Када се ставка ажурира у једној колекцији, она ће се одразити на све колекције. Само чланови организације са приступом овим колекцијама ће моћи да виде ставке." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Изаберите колекције за доделу" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Не можете да се додате у групу." }, - "unassignedItemsBannerSelfHost": { - "message": "Обавештење: 2. маја 2024. недодељене ставке организације више неће бити видљиве у вашем приказу Сви сефови на свим уређајима и биће им доступне само преко Админ конзоле. Доделите ове ставке колекцији са Админ конзолом да бисте их учинили видљивим." - }, - "unassignedItemsBannerNotice": { - "message": "Напомена: Недодељене ставке организације више нису видљиве у вашем приказу Сви сефови на свим уређајима и сада су доступне само преко Админ конзоле." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Обавештење: 16. маја 2024. недодељене ставке организације више неће бити видљиве у вашем приказу Сви сефови на свим уређајима и биће им доступне само преко Админ конзоле." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Избриши провајдера" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Уверите се да чланови имају приступ правим акредитивима и да су њихови налози сигурни. Користите овај извештај да бисте добили ЦСВ приступ чланова и конфигурације налога." }, + "memberAccessReportPageDesc": { + "message": "Провера приступа чланова организације кроз групе, колекције и ставке колекције. ЦСВ извоз пружа детаљну анализу по члану, укључујући информације о дозволама за прикупљање и конфигурацијама налога." + }, "higherKDFIterations": { "message": "Веће KDF итерације може помоћи у заштити ваше главне лозинке од грубе присиле од стране нападача." }, @@ -8522,7 +8514,49 @@ "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." }, "noInvoicesToList": { - "message": "There are no invoices to list", + "message": "Нема фактура за попис", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Обавештење: Касније овог месеца, приватност сефа клијента ће бити побољшана и чланови провајдера више неће имати директан приступ ставкама клијентског сефа. За питања,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "контактирајте подршку Bitwarden-а.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Спонзорисано" + }, + "licenseAndBillingManagementDesc": { + "message": "Након ажурирања на Bitwarden клауду серверу, отпремите датотеку лиценце да бисте применили најновије промене." + }, + "addToFolder": { + "message": "Додај фасцикли" + }, + "selectFolder": { + "message": "Изабери фасциклу" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ биће трајно пребачени у изабрану организацију. Више нећете имати ове ставке.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ биће трајно пребачени у $ORG$. Више нећете имати ове ставке.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 10249636e2e..574f1c9a3dd 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Imejl Adresa" }, - "yourVaultIsLocked": { - "message": "Vaš trezor je zaključan. Unesite glavnu lozinku da biste nastavili." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Ključ" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 9c82c74b41d..206bd766dc7 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-postadress" }, - "yourVaultIsLocked": { - "message": "Valvet är låst. Bekräfta ditt huvudlösenord för att fortsätta." + "yourVaultIsLockedV2": { + "message": "Ditt valv är låst." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Är du säker på att du vill fortsätta?" }, "moveSelectedItemsDesc": { - "message": "Välj en mapp som du vill flytta de $COUNT$ markerade objekten till.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Nyckel" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "E-postadressen har verifierats" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Det gick inte att verifiera din e-postadress. Prova att skicka ett nytt verifieringsmeddelande." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Välj samlingar att tilldela" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Du kan inte lägga till dig själv i en grupp." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Radera leverantör" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Lägg till i mapp" + }, + "selectFolder": { + "message": "Välj mapp" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index bec3ddfbae9..a0393577921 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index a04a84caf03..e2a456a04ef 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "ที่อยู่อีเมล์" }, - "yourVaultIsLocked": { - "message": "ตู้เซฟของคุณถูกล็อค ใส่รหัสผ่านหลักของคุณเพื่อดำเนินการต่อ" + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Are you sure you want to continue?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to move the $COUNT$ selected item(s) to.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Key" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 6725115d714..27bd265dd99 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "E-posta adresi" }, - "yourVaultIsLocked": { - "message": "Kasanız kilitli. Devam etmek için ana parolanızı doğrulayın." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -989,7 +989,7 @@ "message": "Yubico OTP security key" }, "yubiKeyDesc": { - "message": "Hesabınıza erişmek için YubiKey kullanabilirsiniz. YubiKey 4 serisi, 5 serisi ve NEO cihazlarıyla çalışır." + "message": "YubiKey 4, 5 veya NEO cihazı kullanın." }, "duoDescV2": { "message": "Enter a code generated by Duo Security.", @@ -1006,10 +1006,10 @@ "message": "FIDO U2F güvenlik anahtarı" }, "webAuthnTitle": { - "message": "FIDO2 WebAuthn" + "message": "Geçiş anahtarı" }, "webAuthnDesc": { - "message": "Hesabınıza erişmek için WebAuthn uyumlu bir güvenlik anahtarı kullanın." + "message": "Cihazınızın biyometri özelliğini veya FIDO2 uyumlu bir güvenlik anahtarı kullanın." }, "webAuthnMigrated": { "message": "(FIDO'dan taşındı)" @@ -1060,7 +1060,7 @@ "message": "Devam etmek istediğinizden emin misiniz?" }, "moveSelectedItemsDesc": { - "message": "Seçtiğiniz $COUNT$ kaydı taşımak istediğiniz klasörü seçin.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1613,10 +1613,10 @@ "description": "Two-step login providers such as YubiKey, Duo, Authenticator apps, Email, etc." }, "enable": { - "message": "Aç" + "message": "Etkinleştir" }, "enabled": { - "message": "Açıldı" + "message": "Etkinleştirildi" }, "restoreAccess": { "message": "Erişimi geri getir" @@ -1647,7 +1647,7 @@ "message": "Yönetebilir" }, "disable": { - "message": "Kapat" + "message": "Devre dışı bırak" }, "revokeAccess": { "message": "Erişimi iptal et" @@ -1683,7 +1683,7 @@ "message": "You are leaving Bitwarden and launching an external website in a new window." }, "twoStepContinueToBitwardenUrlTitle": { - "message": "Continue to bitwarden.com?" + "message": "bitwarden.com'a gitmek ister misiniz?" }, "twoStepContinueToBitwardenUrlDesc": { "message": "Bitwarden Authenticator allows you to store authenticator keys and generate TOTP codes for 2-step verification flows. Learn more on the bitwarden.com website." @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Anahtar" }, @@ -1704,7 +1707,7 @@ "message": "Bu iki aşamalı giriş sağlayıcısını kaptmak istediğinizden emin misiniz?" }, "twoStepDisabled": { - "message": "İki aşamalı giriş sağlayıcısı kapatıldı." + "message": "İki aşamalı giriş sağlayıcısı devre dışı bırakıldı." }, "twoFactorYubikeyAdd": { "message": "Hesabıma yeni bir YubiKey ekle" @@ -2899,7 +2902,7 @@ "message": "İki aşamalı giriş kaydedildi" }, "disabled2fa": { - "message": "İki aşamalı giriş kapatıldı" + "message": "İki aşamalı giriş devre dışı bırakıldı" }, "recovered2fa": { "message": "Hesap iki aşamalı girişten kurtarıldı." @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Hesap e-postası doğrulandı" }, + "emailVerifiedV2": { + "message": "E-posta doğrulandı" + }, "emailVerifiedFailed": { "message": "E-posta hesabı doğrulanamadı. Yeniden doğrulama e-postası göndermeyi deneyin." }, @@ -5577,10 +5583,10 @@ "message": "\"TOA ve Anahtar Bağlayıcı Şifre Çözme ile Oturum Açma\" etkinleştirildi. Bu politika yalnızca Sahipler ve Yöneticiler için geçerli olacaktır." }, "enabledSso": { - "message": "SSO açıldı" + "message": "SSO etkinleştirildi" }, "disabledSso": { - "message": "SSO kapatıldı" + "message": "SSO etkinleştirildi" }, "enabledKeyConnector": { "message": "Key Connector etkinleştirildi" @@ -5655,10 +5661,10 @@ "message": "Faturalandırma Senkronizasyonu Anahtarını yenilemek, önceki anahtarı geçersiz kılar." }, "selfHostedServer": { - "message": "self-hosted" + "message": "şirket içinde barındırılan" }, "customEnvironment": { - "message": "Custom environment" + "message": "Özel ortam" }, "selfHostedBaseUrlHint": { "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" @@ -5670,22 +5676,22 @@ "message": "You must add either the base Server URL or at least one custom environment." }, "apiUrl": { - "message": "API server URL" + "message": "API sunucusu URL'si" }, "webVaultUrl": { - "message": "Web vault server URL" + "message": "Web kasası sunucu URL'si" }, "identityUrl": { - "message": "Identity server URL" + "message": "Kimlik sunucusu URL'si" }, "notificationsUrl": { - "message": "Notifications server URL" + "message": "Bildirim sunucusu URL'si" }, "iconsUrl": { - "message": "Icons server URL" + "message": "Simge sunucusu URL'si" }, "environmentSaved": { - "message": "Environment URLs saved" + "message": "Ortam URL'leri kaydedildi" }, "selfHostingTitle": { "message": "Barındırılan" @@ -6058,7 +6064,7 @@ "message": "Cihaz doğrulama" }, "enableDeviceVerification": { - "message": "Cihaz doğrulamasını aç" + "message": "Cihaz doğrulamasını etkinleştir" }, "deviceVerificationDesc": { "message": "Tanınmayan bir cihazdan oturum açarken e-posta adresinize doğrulama kodları gönderilir" @@ -6214,7 +6220,7 @@ "message": "Duo'yu başlat" }, "turnOn": { - "message": "Aç" + "message": "Etkinleştir" }, "on": { "message": "Açık" @@ -7877,7 +7883,7 @@ "message": "Bu koleksiyonlara ata" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Öğelerin paylaşılacağı koleksiyonları seçin. Bir koleksiyondaki bir öğe güncellendiğinde tüm koleksiyonlara yansıtılacaktır. Öğeleri yalnızca bu koleksiyonlara erişimi olan kuruluş üyeleri görebilir." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Atanacak koleksiyonları seçin" @@ -7902,7 +7908,7 @@ } }, "items": { - "message": "Ögeler" + "message": "Kayıtlar" }, "assignedSeats": { "message": "Assigned seats" @@ -7938,7 +7944,7 @@ "message": "Subscription update failed" }, "trial": { - "message": "Trial", + "message": "Deneme", "description": "A subscription status label." }, "pastDue": { @@ -7946,7 +7952,7 @@ "description": "A subscription status label" }, "subscriptionExpired": { - "message": "Subscription expired", + "message": "Abonelik sona erdi", "description": "The date header used when a subscription is past due." }, "pastDueWarningForChargeAutomatically": { @@ -7986,7 +7992,7 @@ "description": "The body of a warning box shown to a user whose subscription is unpaid." }, "cancellationDate": { - "message": "Cancellation date", + "message": "İptal tarihi", "description": "The date header used when a subscription is cancelled." }, "machineAccountsCannotCreate": { @@ -8037,7 +8043,7 @@ "message": "Deleting machine accounts is permanent and irreversible." }, "deleteMachineAccountsConfirmMessage": { - "message": "Delete $COUNT$ machine accounts", + "message": "$COUNT$ makine hesabını sil", "placeholders": { "count": { "content": "$1", @@ -8046,60 +8052,60 @@ } }, "deleteMachineAccountToast": { - "message": "Machine account deleted" + "message": "Makine hesabı silindi" }, "deleteMachineAccountsToast": { - "message": "Machine accounts deleted" + "message": "Makine hesapları silindi" }, "searchMachineAccounts": { - "message": "Search machine accounts", + "message": "Makine hesaplarında ara", "description": "Placeholder text for searching machine accounts." }, "editMachineAccount": { - "message": "Edit machine account", + "message": "Makine hesabını düzenle", "description": "Title for editing a machine account." }, "machineAccountName": { - "message": "Machine account name", + "message": "Makine hesabı adı", "description": "Label for the name of a machine account" }, "machineAccountCreated": { - "message": "Machine account created", + "message": "Makine hesabı oluşturuldu", "description": "Notifies that a new machine account has been created" }, "machineAccountUpdated": { - "message": "Machine account updated", + "message": "Makine hesabı güncellendi", "description": "Notifies that a machine account has been updated" }, "projectMachineAccountsDescription": { - "message": "Grant machine accounts access to this project." + "message": "Makine hesaplarına bu projeye erişim izni verin." }, "projectMachineAccountsSelectHint": { - "message": "Type or select machine accounts" + "message": "Makine hesaplarını yazın veya seçin" }, "projectEmptyMachineAccountAccessPolicies": { - "message": "Add machine accounts to grant access" + "message": "Makine vermek için hizmet hesapları ekleyin" }, "machineAccountPeopleDescription": { - "message": "Grant groups or people access to this machine account." + "message": "Gruplara veya kişilere bu makine hesabına erişim izni verin." }, "machineAccountProjectsDescription": { - "message": "Assign projects to this machine account. " + "message": "Bu makine hesabına projeler atayın. " }, "createMachineAccount": { - "message": "Create a machine account" + "message": "Makine hesabı oluştur" }, "maPeopleWarningMessage": { "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." }, "smAccessRemovalWarningMaTitle": { - "message": "Remove access to this machine account" + "message": "Bu makine hesabına erişimi kaldır" }, "smAccessRemovalWarningMaMessage": { - "message": "This action will remove your access to the machine account." + "message": "Bu işlem, makine hesabına erişiminizi kaldıracaktır." }, "machineAccountsIncluded": { - "message": "$COUNT$ machine accounts included", + "message": "$COUNT$ makine hesabı dahil", "placeholders": { "count": { "content": "$1", @@ -8108,7 +8114,7 @@ } }, "additionalMachineAccountCost": { - "message": "$COST$ per month for additional machine accounts", + "message": "İlave makine hesapları için aylık $COST$", "placeholders": { "cost": { "content": "$1", @@ -8117,7 +8123,7 @@ } }, "additionalMachineAccounts": { - "message": "Additional machine accounts" + "message": "İlave makine hesapları" }, "includedMachineAccounts": { "message": "Your plan comes with $COUNT$ machine accounts.", @@ -8150,27 +8156,10 @@ "message": "Max potential machine account cost" }, "machineAccountAccessUpdated": { - "message": "Machine account access updated" + "message": "Makine hesabı erişimi güncellendi" }, "restrictedGroupAccessDesc": { - "message": "You cannot add yourself to a group." - }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + "message": "Kendinizi gruba ekleyemezsiniz." }, "deleteProvider": { "message": "Delete provider" @@ -8219,7 +8208,7 @@ "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Entegrasyonlar" }, "integrationsDesc": { "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." @@ -8273,10 +8262,10 @@ "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." }, "selectAPlan": { - "message": "Select a plan" + "message": "Bir plan seçin" }, "thirtyFivePercentDiscount": { - "message": "35% İndirim" + "message": "35% indirim" }, "monthPerMember": { "message": "month per member" @@ -8285,13 +8274,13 @@ "message": "Seats" }, "addOrganization": { - "message": "Add organization" + "message": "Kuruluş ekle" }, "createdNewClient": { "message": "Successfully created new client" }, "noAccess": { - "message": "No access" + "message": "Erişim yok" }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" @@ -8329,11 +8318,11 @@ } }, "back": { - "message": "Back", + "message": "Geri", "description": "Button text to navigate back" }, "removeItem": { - "message": "Remove $NAME$", + "message": "$NAME$ klasörünü kaldır", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -8343,10 +8332,10 @@ } }, "viewInfo": { - "message": "View info" + "message": "Bilgileri görüntüle" }, "viewAccess": { - "message": "View access" + "message": "Erişimi görüntüle" }, "noCollectionsSelected": { "message": "You have not selected any collections." @@ -8364,7 +8353,7 @@ "message": "Organization Seats" }, "providerDiscount": { - "message": "%$AMOUNT$ İndirim", + "message": "%$AMOUNT$ indirim", "placeholders": { "amount": { "content": "$1", @@ -8406,10 +8395,10 @@ "message": "Updated tax information" }, "unverified": { - "message": "Unverified" + "message": "Doğrulanmadı" }, "verified": { - "message": "Verified" + "message": "Doğrulandı" }, "viewSecret": { "message": "View secret" @@ -8428,7 +8417,7 @@ "message": "Quickly view member access across the organization by upgrading to an Enterprise plan." }, "date": { - "message": "Date" + "message": "Tarih" }, "exportClientReport": { "message": "Export client report" @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "KDF iterasyonunun daha yüksek olması ana parolanızı kaba kuvvet saldırılarına karşı daha iyi korur." }, @@ -8509,7 +8501,7 @@ "message": "Client details" }, "downloadCSV": { - "message": "Download CSV" + "message": "CSV'yi indir" }, "monthlySubscriptionUserSeatsMessage": { "message": "Adjustments to your subscription will result in prorated charges to your billing totals on your next billing period. " @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "Listelenecek fatura yok", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsorlu" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Klasöre ekle" + }, + "selectFolder": { + "message": "Klasör seç" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index d2602ac38e4..e316cc0322d 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Адреса е-пошти" }, - "yourVaultIsLocked": { - "message": "Сховище заблоковано. Введіть головний пароль для продовження." + "yourVaultIsLockedV2": { + "message": "Ваше сховище заблоковано." }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Ви дійсно хочете продовжити?" }, "moveSelectedItemsDesc": { - "message": "Оберіть теку, в яку ви бажаєте перемістити $COUNT$ вибраних записів.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Скануйте зазначений нижче QR-код за допомогою програми для автентифікації або введіть ключ." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Не вдалося завантажити QR-код. Повторіть спробу або скористайтеся зазначеним нижче ключем." + }, "key": { "message": "Ключ" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Адресу е-пошти підтверджено" }, + "emailVerifiedV2": { + "message": "Електронну пошту підтверджено" + }, "emailVerifiedFailed": { "message": "Неможливо підтвердити вашу е-пошту. Спробуйте надіслати нове повідомлення для підтвердження." }, @@ -7877,7 +7883,7 @@ "message": "Призначити до цих збірок" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Оберіть збірки, в яких поширюватимуться записи. Після оновлення запису в одній збірці зміни буде відображено у всіх збірках. Лише учасники організації з доступом до цих збірок зможуть переглядати записи." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Оберіть збірки для призначення" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "Ви не можете додати себе до групи." }, - "unassignedItemsBannerSelfHost": { - "message": "Сповіщення: 2 травня 2024 року, непризначені елементи організації більше не будуть видимі на ваших пристроях у поданні \"Усі сховища\", і будуть доступні лише через консоль адміністратора. Щоб зробити їх видимими, призначте ці елементи збірці в консолі адміністратора." - }, - "unassignedItemsBannerNotice": { - "message": "Примітка: непризначені елементи організації більше не видимі на ваших пристроях у поданні \"Усі сховища\", і тепер доступні лише через консоль адміністратора." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Примітка: 16 травня 2024 року непризначені елементи організації більше не будуть видимі на ваших пристроях у поданні \"Усі сховища\", і будуть доступні лише через консоль адміністратора." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Призначте ці елементи збірці в", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "щоб зробити їх видимими.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Видалити провайдера" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Переконайтеся, що учасники мають доступ до належних облікових даних і їхні облікові записи надійні. Використовуйте цей звіт, щоб отримати файл CSV з даними про доступ учасників та конфігурації облікових записів." }, + "memberAccessReportPageDesc": { + "message": "Аудит доступу учасника організації до груп, збірок та елементів збірок. Експорт CSV надає детальну розбивку для кожного учасника, зокрема інформацію про дозволи для збірок та конфігурації облікового запису." + }, "higherKDFIterations": { "message": "Вищі значення KDF-ітерацій можуть допомогти захистити ваш головний пароль від грубого зламу зловмисником." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "Немає рахунків для показу", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Повідомлення: пізніше цього місяця приватність клієнтського сховища буде поліпшено й учасники провайдера більше не матимуть прямого доступу до елементів клієнтських сховищ. Для отримання відповідей на запитання,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "зверніться до служби підтримки Bitwarden.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Спонсоровано" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index b82c86b1476..7f61058de48 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Địa chỉ email" }, - "yourVaultIsLocked": { - "message": "Kho của bạn đã bị khóa. Nhập mật khẩu chính để tiếp tục." + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "Bạn có chắc rằng bạn muốn tiếp tục?" }, "moveSelectedItemsDesc": { - "message": "Chọn thư mục mà bạn muốn di chuyển $COUNT$ mục này tới.", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "Khóa" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "Account email verified" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "Unable to verify your email. Try sending a new verification email." }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "Delete provider" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index e327eb35a93..462dc05dbfd 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "电子邮件地址" }, - "yourVaultIsLocked": { - "message": "您的密码库已锁定,请验证您的主密码以继续。" + "yourVaultIsLockedV2": { + "message": "您的密码库已锁定" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "确定要继续吗?" }, "moveSelectedItemsDesc": { - "message": "选择要将这 $COUNT$ 个项目移动到的文件夹。", + "message": "选择一个您想要将这 $COUNT$ 个所选项目添加到的文件夹。", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "用您的验证器 App 扫描下面的二维码或输入密钥。" }, + "twoStepAuthenticatorQRCanvasError": { + "message": "无法加载二维码。请重试或使用下面的密钥。" + }, "key": { "message": "密钥" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "账户电子邮件已验证" }, + "emailVerifiedV2": { + "message": "电子邮箱已验证" + }, "emailVerifiedFailed": { "message": "无法验证您的电子邮件。尝试发送新的验证电子邮件。" }, @@ -7877,7 +7883,7 @@ "message": "分配到这些集合" }, "bulkCollectionAssignmentDialogDescription": { - "message": "选择与其共享项目的集合。当一个项目在某个集合中更新后,它将反映到所有集合中。只有具有这些集合访问权限的组织成员才能看到这些项目。" + "message": "只有具有这些集合访问权限的组织成员才能看到这些项目。" }, "selectCollectionsToAssign": { "message": "选择要分配的集合" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "您不能将自己添加到群组。" }, - "unassignedItemsBannerSelfHost": { - "message": "注意:从 2024 年 5 月 2 日起,未分配的组织项目在您所有设备的「所有密码库」视图中将不再可见,只能通过管理控制台访问。通过管理控制台将这些项目分配给集合以使其可见。" - }, - "unassignedItemsBannerNotice": { - "message": "注意:未分配的组织项目在您所有设备的「所有密码库」视图中不再可见,现在只能通过管理控制台访问。" - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "注意:从 2024 年 5 月 2 日起,未分配的组织项目在您所有设备的「所有密码库」视图中将不再可见,只能通过管理控制台访问。" - }, - "unassignedItemsBannerCTAPartOne": { - "message": "将这些项目分配到集合,通过", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ",以使其可见。", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "删除提供商" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "确保成员具有对合适的凭据的访问权限,以及他们的账户是安全的。使用此报告获取包含会员访问权限和账户配置的 CSV 文件 。" }, + "memberAccessReportPageDesc": { + "message": "审计组织成员在各个群组、集合和集合项目之间的访问权限。CSV 导出文件提供了每位成员的详细信息,包括集合权限和账户配置的相关信息。" + }, "higherKDFIterations": { "message": "更高的 KDF 迭代可以帮助保护您的主密码免遭攻击者的暴力破解。" }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "没有可列出的账单", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "注意:本月晚些时候,客户密码库隐私将被改进,提供商成员将不再能够直接访问客户密码库项目。如有疑问,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "联系 Bitwarden 支持。", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "赞助" + }, + "licenseAndBillingManagementDesc": { + "message": "在 Bitwarden 云服务器中进行更新后,上传许可证文件以应用最新的更改。" + }, + "addToFolder": { + "message": "添加到文件夹" + }, + "selectFolder": { + "message": "选择文件夹" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ 将永久转移到所选组织。您将不再拥有这些项目。", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ 将永久转移到 $ORG$。您将不再拥有这些项目。", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 1892eb0801a..7120eb05db9 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "電子郵件地址" }, - "yourVaultIsLocked": { - "message": "密碼庫已鎖定。請驗證主密碼以繼續。" + "yourVaultIsLockedV2": { + "message": "Your vault is locked" }, "uuid": { "message": "UUID" @@ -1060,7 +1060,7 @@ "message": "您確定要繼續嗎?" }, "moveSelectedItemsDesc": { - "message": "選擇要將這 $COUNT$ 個項目移動至哪個資料夾。", + "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", "placeholders": { "count": { "content": "$1", @@ -1691,6 +1691,9 @@ "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." }, + "twoStepAuthenticatorQRCanvasError": { + "message": "Could not load QR code. Try again or use the key below." + }, "key": { "message": "金鑰" }, @@ -3399,6 +3402,9 @@ "emailVerified": { "message": "帳戶電子郵件已驗證" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerifiedFailed": { "message": "無法驗證電子郵件。請嘗試傳送一封新的驗證電子郵件。" }, @@ -7877,7 +7883,7 @@ "message": "Assign to these collections" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Only organization members with access to these collections will be able to see the items." }, "selectCollectionsToAssign": { "message": "Select collections to assign" @@ -8155,23 +8161,6 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "deleteProvider": { "message": "刪除提供者" }, @@ -8439,6 +8428,9 @@ "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." }, + "memberAccessReportPageDesc": { + "message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations." + }, "higherKDFIterations": { "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." }, @@ -8524,5 +8516,47 @@ "noInvoicesToList": { "message": "There are no invoices to list", "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "providerClientVaultPrivacyNotification": { + "message": "Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions,", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'." + }, + "contactBitwardenSupport": { + "message": "contact Bitwarden support.", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" + }, + "licenseAndBillingManagementDesc": { + "message": "After making updates in the Bitwarden cloud server, upload your license file to apply the most recent changes." + }, + "addToFolder": { + "message": "Add to folder" + }, + "selectFolder": { + "message": "Select folder" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } } } diff --git a/apps/web/src/scss/plugins.scss b/apps/web/src/scss/plugins.scss index 40ba692c463..f4aa428532c 100644 --- a/apps/web/src/scss/plugins.scss +++ b/apps/web/src/scss/plugins.scss @@ -108,6 +108,13 @@ border: none; } +// hide duplicate paypal iframe +.braintree-sheet__content--button + .braintree-sheet__button--paypal + iframe.zoid-prerender-frame.zoid-invisible { + display: none !important; +} + [data-braintree-id="upper-container"]::before { @include themify($themes) { background-color: themed("backgroundColor"); diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html index 1d71deca122..dee663eb199 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html @@ -10,7 +10,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.html index 4d4c7f11076..220a2214600 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.html @@ -21,7 +21,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }} + + + {{ title | uppercase }} + {{ dialogParams.user.name }} + +
+ +

{{ "providerInviteUserDesc" | i18n }}

+
+ + + {{ "email" | i18n }} + + + {{ "inviteMultipleEmailDesc" | i18n: "20" }} + +
+
+ +

+ {{ "userType" | i18n | uppercase }} + + + +

+ + + + {{ "serviceUser" | i18n }} + + {{ "serviceUserDesc" | i18n }} + + + + {{ "providerAdmin" | i18n }} + + {{ "providerAdminDesc" | i18n }} + + +
+
+ + + +
+ +
+
+
+ diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.ts new file mode 100644 index 00000000000..5f88bf177ca --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.ts @@ -0,0 +1,127 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ProviderUserType } from "@bitwarden/common/admin-console/enums"; +import { ProviderUserInviteRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-invite.request"; +import { ProviderUserUpdateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-update.request"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService, ToastService } from "@bitwarden/components"; + +export type AddEditMemberDialogParams = { + providerId: string; + user?: { + id: string; + name: string; + type: ProviderUserType; + }; +}; + +export enum AddEditMemberDialogResultType { + Closed = "closed", + Deleted = "deleted", + Saved = "saved", +} + +@Component({ + templateUrl: "add-edit-member-dialog.component.html", +}) +export class AddEditMemberDialogComponent { + editing = false; + loading = true; + title: string; + + protected ResultType = AddEditMemberDialogResultType; + protected UserType = ProviderUserType; + + protected formGroup = new FormGroup({ + emails: new FormControl("", [Validators.required]), + type: new FormControl(this.dialogParams.user?.type ?? ProviderUserType.ServiceUser), + }); + + constructor( + private apiService: ApiService, + @Inject(DIALOG_DATA) protected dialogParams: AddEditMemberDialogParams, + private dialogRef: DialogRef, + private dialogService: DialogService, + private i18nService: I18nService, + private toastService: ToastService, + ) { + this.editing = this.loading = this.dialogParams.user != null; + if (this.editing) { + this.title = this.i18nService.t("editMember"); + const emailControl = this.formGroup.controls.emails; + emailControl.removeValidators(Validators.required); + emailControl.disable(); + } else { + this.title = this.i18nService.t("inviteMember"); + } + + this.loading = false; + } + + delete = async (): Promise => { + if (!this.editing) { + return; + } + + const confirmed = await this.dialogService.openSimpleDialog({ + title: this.dialogParams.user.name, + content: { key: "removeUserConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + await this.apiService.deleteProviderUser( + this.dialogParams.providerId, + this.dialogParams.user.id, + ); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("removedUserId", this.dialogParams.user.name), + }); + + this.dialogRef.close(AddEditMemberDialogResultType.Deleted); + }; + + submit = async (): Promise => { + if (this.editing) { + const request = new ProviderUserUpdateRequest(); + request.type = this.formGroup.value.type; + await this.apiService.putProviderUser( + this.dialogParams.providerId, + this.dialogParams.user.id, + request, + ); + } else { + const request = new ProviderUserInviteRequest(); + request.emails = this.formGroup.value.emails.trim().split(/\s*,\s*/); + request.type = this.formGroup.value.type; + await this.apiService.postProviderUserInvite(this.dialogParams.providerId, request); + } + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t( + this.editing ? "editedUserId" : "invitedUsers", + this.dialogParams.user?.name, + ), + }); + + this.dialogRef.close(AddEditMemberDialogResultType.Saved); + }; + + static open(dialogService: DialogService, dialogConfig: DialogConfig) { + return dialogService.open( + AddEditMemberDialogComponent, + dialogConfig, + ); + } +} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts new file mode 100644 index 00000000000..d4a179091aa --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts @@ -0,0 +1,69 @@ +import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { + OrganizationUserBulkPublicKeyResponse, + OrganizationUserBulkResponse, +} from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; +import { ProviderUserStatusType } from "@bitwarden/common/admin-console/enums"; +import { ProviderUserBulkConfirmRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk-confirm.request"; +import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request"; +import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk-public-key.response"; +import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { DialogService } from "@bitwarden/components"; +import { BaseBulkConfirmComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component"; +import { BulkUserDetails } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-status.component"; + +type BulkConfirmDialogParams = { + providerId: string; + users: BulkUserDetails[]; +}; + +@Component({ + templateUrl: + "../../../../../../../../apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm.component.html", +}) +export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent { + providerId: string; + + constructor( + private apiService: ApiService, + protected cryptoService: CryptoService, + @Inject(DIALOG_DATA) protected dialogParams: BulkConfirmDialogParams, + protected i18nService: I18nService, + ) { + super(cryptoService, i18nService); + + this.providerId = dialogParams.providerId; + this.users = dialogParams.users; + } + + protected getCryptoKey = (): Promise => + this.cryptoService.getProviderKey(this.providerId); + + protected getPublicKeys = async (): Promise< + ListResponse + > => { + const request = new ProviderUserBulkRequest(this.filteredUsers.map((user) => user.id)); + return await this.apiService.postProviderUsersPublicKey(this.providerId, request); + }; + + protected isAccepted = (user: BulkUserDetails): boolean => + user.status === ProviderUserStatusType.Accepted; + + protected postConfirmRequest = async ( + userIdsWithKeys: { id: string; key: string }[], + ): Promise> => { + const request = new ProviderUserBulkConfirmRequest(userIdsWithKeys); + return await this.apiService.postProviderUserBulkConfirm(this.providerId, request); + }; + + static open(dialogService: DialogService, dialogConfig: DialogConfig) { + return dialogService.open(BulkConfirmDialogComponent, dialogConfig); + } +} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts new file mode 100644 index 00000000000..16e64703700 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts @@ -0,0 +1,49 @@ +import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request"; +import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService } from "@bitwarden/components"; +import { BaseBulkRemoveComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/base-bulk-remove.component"; +import { BulkUserDetails } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-status.component"; + +type BulkRemoveDialogParams = { + providerId: string; + users: BulkUserDetails[]; +}; + +@Component({ + templateUrl: + "../../../../../../../../apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove.component.html", +}) +export class BulkRemoveDialogComponent extends BaseBulkRemoveComponent { + providerId: string; + users: BulkUserDetails[]; + + constructor( + private apiService: ApiService, + @Inject(DIALOG_DATA) dialogParams: BulkRemoveDialogParams, + protected i18nService: I18nService, + ) { + super(i18nService); + + this.providerId = dialogParams.providerId; + this.users = dialogParams.users; + } + + protected deleteUsers = (): Promise> => { + const request = new ProviderUserBulkRequest(this.users.map((user) => user.id)); + return this.apiService.deleteManyProviderUsers(this.providerId, request); + }; + + protected get removeUsersWarning() { + return this.i18nService.t("removeOrgUsersConfirmation"); + } + + static open(dialogService: DialogService, dialogConfig: DialogConfig) { + return dialogService.open(BulkRemoveDialogComponent, dialogConfig); + } +} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.html new file mode 100644 index 00000000000..66c42678442 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.html @@ -0,0 +1,225 @@ + + + + + + +
+ + + {{ "all" | i18n }} + + {{ allCount }} + + + + {{ "invited" | i18n }} + + {{ invitedCount }} + + + + {{ "needsConfirmation" | i18n }} + + {{ acceptedCount }} + + + +
+ + + + {{ "loading" | i18n }} + + + +

{{ "noMembersInList" | i18n }}

+ + + {{ "providerUsersNeedConfirmed" | i18n }} + + + + + + + + + + {{ "name" | i18n }} + {{ "role" | i18n }} + + + + + + + + + + + + + + + + +
+ +
+
+ + + {{ "invited" | i18n }} + + + {{ "needsConfirmation" | i18n }} + + + {{ "revoked" | i18n }} + +
+
+ {{ user.email }} +
+
+
+ + + {{ "providerAdmin" | i18n }} + {{ "serviceUser" | i18n }} + + + + + + + + + + + +
+
+
+
+
diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts new file mode 100644 index 00000000000..247297ff962 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts @@ -0,0 +1,243 @@ +import { DialogRef } from "@angular/cdk/dialog"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ActivatedRoute, Router } from "@angular/router"; +import { combineLatest, lastValueFrom, switchMap } from "rxjs"; +import { first } from "rxjs/operators"; + +import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; +import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; +import { + OrganizationUserStatusType, + ProviderUserStatusType, + ProviderUserType, +} from "@bitwarden/common/admin-console/enums"; +import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request"; +import { ProviderUserConfirmRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-confirm.request"; +import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { DialogService, ToastService } from "@bitwarden/components"; +import { BaseMembersComponent } from "@bitwarden/web-vault/app/admin-console/common/base-members.component"; +import { + peopleFilter, + PeopleTableDataSource, +} from "@bitwarden/web-vault/app/admin-console/common/people-table-data-source"; +import { openEntityEventsDialog } from "@bitwarden/web-vault/app/admin-console/organizations/manage/entity-events.component"; +import { BulkStatusComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-status.component"; + +import { + AddEditMemberDialogComponent, + AddEditMemberDialogParams, + AddEditMemberDialogResultType, +} from "./dialogs/add-edit-member-dialog.component"; +import { BulkConfirmDialogComponent } from "./dialogs/bulk-confirm-dialog.component"; +import { BulkRemoveDialogComponent } from "./dialogs/bulk-remove-dialog.component"; + +type ProviderUser = ProviderUserUserDetailsResponse; + +class MembersTableDataSource extends PeopleTableDataSource { + protected statusType = OrganizationUserStatusType; +} + +@Component({ + templateUrl: "members.component.html", +}) +export class MembersComponent extends BaseMembersComponent { + accessEvents = false; + dataSource = new MembersTableDataSource(); + loading = true; + providerId: string; + rowHeight = 62; + rowHeightClass = `tw-h-[62px]`; + status: ProviderUserStatusType = null; + + userStatusType = ProviderUserStatusType; + userType = ProviderUserType; + + constructor( + apiService: ApiService, + cryptoService: CryptoService, + dialogService: DialogService, + i18nService: I18nService, + logService: LogService, + organizationManagementPreferencesService: OrganizationManagementPreferencesService, + toastService: ToastService, + userNamePipe: UserNamePipe, + validationService: ValidationService, + private activatedRoute: ActivatedRoute, + private providerService: ProviderService, + private router: Router, + ) { + super( + apiService, + i18nService, + cryptoService, + validationService, + logService, + userNamePipe, + dialogService, + organizationManagementPreferencesService, + toastService, + ); + + combineLatest([ + this.activatedRoute.parent.params, + this.activatedRoute.queryParams.pipe(first()), + ]) + .pipe( + switchMap(async ([urlParams, queryParams]) => { + this.searchControl.setValue(queryParams.search, { emitEvent: false }); + this.dataSource.filter = peopleFilter(queryParams.search, null); + + this.providerId = urlParams.providerId; + const provider = await this.providerService.get(this.providerId); + if (!provider || !provider.canManageUsers) { + return await this.router.navigate(["../"], { relativeTo: this.activatedRoute }); + } + this.accessEvents = provider.useEvents; + await this.load(); + + if (queryParams.viewEvents != null) { + const user = this.dataSource.data.find((user) => user.id === queryParams.viewEvents); + if (user && user.status === ProviderUserStatusType.Confirmed) { + this.openEventsDialog(user); + } + } + }), + takeUntilDestroyed(), + ) + .subscribe(); + } + + async bulkConfirm(): Promise { + if (this.actionPromise != null) { + return; + } + + const dialogRef = BulkConfirmDialogComponent.open(this.dialogService, { + data: { + providerId: this.providerId, + users: this.dataSource.getCheckedUsers(), + }, + }); + + await lastValueFrom(dialogRef.closed); + await this.load(); + } + + async bulkReinvite(): Promise { + if (this.actionPromise != null) { + return; + } + + const checkedUsers = this.dataSource.getCheckedUsers(); + const checkedInvitedUsers = checkedUsers.filter( + (user) => user.status === ProviderUserStatusType.Invited, + ); + + if (checkedInvitedUsers.length <= 0) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("noSelectedUsersApplicable"), + }); + return; + } + + try { + const request = this.apiService.postManyProviderUserReinvite( + this.providerId, + new ProviderUserBulkRequest(checkedInvitedUsers.map((user) => user.id)), + ); + + const dialogRef = BulkStatusComponent.open(this.dialogService, { + data: { + users: checkedUsers, + filteredUsers: checkedInvitedUsers, + request, + successfulMessage: this.i18nService.t("bulkReinviteMessage"), + }, + }); + await lastValueFrom(dialogRef.closed); + } catch (error) { + this.validationService.showError(error); + } + } + + async bulkRemove(): Promise { + if (this.actionPromise != null) { + return; + } + + const dialogRef = BulkRemoveDialogComponent.open(this.dialogService, { + data: { + providerId: this.providerId, + users: this.dataSource.getCheckedUsers(), + }, + }); + + await lastValueFrom(dialogRef.closed); + await this.load(); + } + + async confirmUser(user: ProviderUser, publicKey: Uint8Array): Promise { + const providerKey = await this.cryptoService.getProviderKey(this.providerId); + const key = await this.cryptoService.rsaEncrypt(providerKey.key, publicKey); + const request = new ProviderUserConfirmRequest(); + request.key = key.encryptedString; + await this.apiService.postProviderUserConfirm(this.providerId, user.id, request); + } + + deleteUser = (id: string): Promise => + this.apiService.deleteProviderUser(this.providerId, id); + + edit = async (user: ProviderUser | null): Promise => { + const data: AddEditMemberDialogParams = { + providerId: this.providerId, + }; + + if (user != null) { + data.user = { + id: user.id, + name: this.userNamePipe.transform(user), + type: user.type, + }; + } + + const dialogRef = AddEditMemberDialogComponent.open(this.dialogService, { + data, + }); + + const result = await lastValueFrom(dialogRef.closed); + + switch (result) { + case AddEditMemberDialogResultType.Saved: + case AddEditMemberDialogResultType.Deleted: + await this.load(); + break; + } + }; + + openEventsDialog = (user: ProviderUser): DialogRef => + openEntityEventsDialog(this.dialogService, { + data: { + name: this.userNamePipe.transform(user), + providerId: this.providerId, + entityId: user.id, + showUser: false, + entity: "user", + }, + }); + + getUsers = (): Promise> => + this.apiService.getProviderUsers(this.providerId); + + reinviteUser = (id: string): Promise => + this.apiService.postProviderUserReinvite(this.providerId, id); +} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.html index b145f16daf1..36bc6543696 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.html @@ -82,7 +82,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}
diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts index 564808d0055..1849809df5f 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts @@ -15,6 +15,7 @@ import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/ import { ProviderUserConfirmRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-confirm.request"; import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -29,6 +30,9 @@ import { BulkConfirmComponent } from "./bulk/bulk-confirm.component"; import { BulkRemoveComponent } from "./bulk/bulk-remove.component"; import { UserAddEditComponent } from "./user-add-edit.component"; +/** + * @deprecated Please use the {@link MembersComponent} instead. + */ @Component({ selector: "provider-people", templateUrl: "people.component.html", @@ -70,6 +74,7 @@ export class PeopleComponent private providerService: ProviderService, dialogService: DialogService, organizationManagementPreferencesService: OrganizationManagementPreferencesService, + private configService: ConfigService, ) { super( apiService, @@ -228,7 +233,7 @@ export class PeopleComponent users: users, filteredUsers: filteredUsers, request: response, - successfullMessage: this.i18nService.t("bulkReinviteMessage"), + successfulMessage: this.i18nService.t("bulkReinviteMessage"), }, }); await lastValueFrom(dialogRef.closed); diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.html index 11f6ae07d83..78d80d005c9 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.html @@ -27,7 +27,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html index 0fd6725304c..3b81d0564c9 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html @@ -7,7 +7,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}
diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts index e97e4ea9596..f92223d1b54 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts @@ -36,7 +36,10 @@ export const openManageClientSubscriptionDialog = ( export class ManageClientSubscriptionDialogComponent implements OnInit { protected loading = true; protected providerPlan: ProviderPlanResponse; + protected assignedSeats: number; protected openSeats: number; + protected purchasedSeats: number; + protected seatMinimum: number; protected readonly ResultType = ManageClientSubscriptionDialogResultType; protected formGroup = new FormGroup({ @@ -63,7 +66,10 @@ export class ManageClientSubscriptionDialogComponent implements OnInit { (plan) => plan.planName === this.dialogParams.organization.plan, ); + this.assignedSeats = this.providerPlan.assignedSeats; this.openSeats = this.providerPlan.seatMinimum - this.providerPlan.assignedSeats; + this.purchasedSeats = this.providerPlan.purchasedSeats; + this.seatMinimum = this.providerPlan.seatMinimum; this.formGroup.controls.assignedSeats.addValidators( this.isServiceUserWithPurchasedSeats @@ -165,9 +171,22 @@ export class ManageClientSubscriptionDialogComponent implements OnInit { const seatDifference = this.formGroup.value.assignedSeats - this.dialogParams.organization.seats; - const purchasedSeats = seatDifference - this.openSeats; + if (this.purchasedSeats > 0) { + return seatDifference; + } - return purchasedSeats > 0 ? purchasedSeats : 0; + return seatDifference - this.openSeats; + } + + get purchasedSeatsRemoved(): number { + const seatDifference = + this.dialogParams.organization.seats - this.formGroup.value.assignedSeats; + + if (this.purchasedSeats >= seatDifference) { + return seatDifference; + } + + return this.purchasedSeats; } get isProviderAdmin(): boolean { @@ -177,4 +196,12 @@ export class ManageClientSubscriptionDialogComponent implements OnInit { get isServiceUserWithPurchasedSeats(): boolean { return !this.isProviderAdmin && this.providerPlan && this.providerPlan.purchasedSeats > 0; } + + get purchasingSeats(): boolean { + return this.additionalSeatsPurchased > 0; + } + + get sellingSeats(): boolean { + return this.purchasedSeats > 0 && this.additionalSeatsPurchased < 0; + } } diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html index e626df3a63f..49a9208107f 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html @@ -12,7 +12,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/payment-method/provider-payment-method.component.html b/bitwarden_license/bit-web/src/app/billing/providers/payment-method/provider-payment-method.component.html index e2457294eb1..2894752b28f 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/payment-method/provider-payment-method.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/payment-method/provider-payment-method.component.html @@ -5,7 +5,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html index 0aaa3cc03ae..5c8a0985797 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html @@ -2,7 +2,7 @@ - {{ "loading" | i18n }} + {{ "loading" | i18n }} {{ "acceptedFormats" | i18n }} Bitwarden (json) diff --git a/libs/angular/src/auth/components/change-password.component.ts b/libs/angular/src/auth/components/change-password.component.ts index 5e9b31138e0..d6c0ec92710 100644 --- a/libs/angular/src/auth/components/change-password.component.ts +++ b/libs/angular/src/auth/components/change-password.component.ts @@ -62,6 +62,10 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { (enforcedPasswordPolicyOptions) => (this.enforcedPolicyOptions ??= enforcedPasswordPolicyOptions), ); + + if (this.enforcedPolicyOptions?.minLength) { + this.minimumLength = this.enforcedPolicyOptions.minLength; + } } ngOnDestroy(): void { diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.html b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.html new file mode 100644 index 00000000000..c9d0901bcae --- /dev/null +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.html @@ -0,0 +1,19 @@ +

+ {{ "enterVerificationCodeEmail" | i18n: twoFactorEmail }} +

+ + {{ "verificationCode" | i18n }} + + + + {{ "sendVerificationCodeEmailAgain" | i18n }} + + diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.ts b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.ts new file mode 100644 index 00000000000..7ac18bbc962 --- /dev/null +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.ts @@ -0,0 +1,109 @@ +import { DialogModule } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, EventEmitter, Output } from "@angular/core"; +import { ReactiveFormsModule, FormsModule } from "@angular/forms"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; +import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; +import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request"; +import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { + ButtonModule, + LinkModule, + TypographyModule, + FormFieldModule, + AsyncActionsModule, +} from "@bitwarden/components"; + +@Component({ + standalone: true, + selector: "app-two-factor-auth-email", + templateUrl: "two-factor-auth-email.component.html", + imports: [ + CommonModule, + JslibModule, + DialogModule, + ButtonModule, + LinkModule, + TypographyModule, + ReactiveFormsModule, + FormFieldModule, + AsyncActionsModule, + FormsModule, + ], + providers: [I18nPipe], +}) +export class TwoFactorAuthEmailComponent { + @Output() token = new EventEmitter(); + + twoFactorEmail: string = null; + emailPromise: Promise; + tokenValue: string = ""; + + constructor( + protected i18nService: I18nService, + protected twoFactorService: TwoFactorService, + protected loginStrategyService: LoginStrategyServiceAbstraction, + protected platformUtilsService: PlatformUtilsService, + protected logService: LogService, + protected apiService: ApiService, + protected appIdService: AppIdService, + ) {} + + async ngOnInit(): Promise { + const providerData = await this.twoFactorService.getProviders().then((providers) => { + return providers.get(TwoFactorProviderType.Email); + }); + this.twoFactorEmail = providerData.Email; + + if ((await this.twoFactorService.getProviders()).size > 1) { + await this.sendEmail(false); + } + } + + async sendEmail(doToast: boolean) { + if (this.emailPromise != null) { + return; + } + + if ((await this.loginStrategyService.getEmail()) == null) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("sessionTimeout"), + ); + return; + } + + try { + const request = new TwoFactorEmailRequest(); + request.email = await this.loginStrategyService.getEmail(); + request.masterPasswordHash = await this.loginStrategyService.getMasterPasswordHash(); + request.ssoEmail2FaSessionToken = + await this.loginStrategyService.getSsoEmail2FaSessionToken(); + request.deviceIdentifier = await this.appIdService.getAppId(); + request.authRequestAccessCode = await this.loginStrategyService.getAccessCode(); + request.authRequestId = await this.loginStrategyService.getAuthRequestId(); + this.emailPromise = this.apiService.postTwoFactorEmail(request); + await this.emailPromise; + if (doToast) { + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("verificationCodeEmailSent", this.twoFactorEmail), + ); + } + } catch (e) { + this.logService.error(e); + } + + this.emailPromise = null; + } +} diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.html b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.html new file mode 100644 index 00000000000..65a7ef9a50e --- /dev/null +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.html @@ -0,0 +1,11 @@ +
+ +
+ +
+

{{ "webAuthnNewTab" | i18n }}

+ +
+
diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.ts b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.ts new file mode 100644 index 00000000000..d6814fa9c06 --- /dev/null +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.ts @@ -0,0 +1,131 @@ +import { DialogModule } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, EventEmitter, Inject, Output } from "@angular/core"; +import { ReactiveFormsModule, FormsModule } from "@angular/forms"; +import { ActivatedRoute } from "@angular/router"; +import { firstValueFrom } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; +import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; +import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; +import { WebAuthnIFrame } from "@bitwarden/common/auth/webauthn-iframe"; +import { ClientType } from "@bitwarden/common/enums"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { + ButtonModule, + LinkModule, + TypographyModule, + FormFieldModule, + AsyncActionsModule, +} from "@bitwarden/components"; + +@Component({ + standalone: true, + selector: "app-two-factor-auth-webauthn", + templateUrl: "two-factor-auth-webauthn.component.html", + imports: [ + CommonModule, + JslibModule, + DialogModule, + ButtonModule, + LinkModule, + TypographyModule, + ReactiveFormsModule, + FormFieldModule, + AsyncActionsModule, + FormsModule, + ], + providers: [I18nPipe], +}) +export class TwoFactorAuthWebAuthnComponent { + @Output() token = new EventEmitter(); + + webAuthnReady = false; + webAuthnNewTab = false; + webAuthnSupported = false; + webAuthn: WebAuthnIFrame = null; + + constructor( + protected i18nService: I18nService, + protected platformUtilsService: PlatformUtilsService, + @Inject(WINDOW) protected win: Window, + protected environmentService: EnvironmentService, + protected twoFactorService: TwoFactorService, + protected route: ActivatedRoute, + ) { + this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); + + if (this.platformUtilsService.getClientType() == ClientType.Browser) { + // FIXME: Chromium 110 has broken WebAuthn support in extensions via an iframe + this.webAuthnNewTab = true; + } + } + + async ngOnInit(): Promise { + if (this.route.snapshot.paramMap.has("webAuthnResponse")) { + this.token.emit(this.route.snapshot.paramMap.get("webAuthnResponse")); + } + + this.cleanupWebAuthn(); + + if (this.win != null && this.webAuthnSupported) { + const env = await firstValueFrom(this.environmentService.environment$); + const webVaultUrl = env.getWebVaultUrl(); + this.webAuthn = new WebAuthnIFrame( + this.win, + webVaultUrl, + this.webAuthnNewTab, + this.platformUtilsService, + this.i18nService, + (token: string) => { + this.token.emit(token); + }, + (error: string) => { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("webauthnCancelOrTimeout"), + ); + }, + (info: string) => { + if (info === "ready") { + this.webAuthnReady = true; + } + }, + ); + + if (!this.webAuthnNewTab) { + setTimeout(async () => { + await this.authWebAuthn(); + }, 500); + } + } + } + + ngOnDestroy(): void { + this.cleanupWebAuthn(); + } + + async authWebAuthn() { + const providerData = (await this.twoFactorService.getProviders()).get( + TwoFactorProviderType.WebAuthn, + ); + + if (!this.webAuthnSupported || this.webAuthn == null) { + return; + } + + this.webAuthn.init(providerData); + } + + private cleanupWebAuthn() { + if (this.webAuthn != null) { + this.webAuthn.stop(); + this.webAuthn.cleanup(); + } + } +} diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html index 1de1561a344..33a5e291faa 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html @@ -1,5 +1,9 @@
+ + {{ "rememberMe" | i18n }} @@ -27,7 +35,7 @@ buttonType="primary" bitButton bitFormButton - *ngIf="selectedProviderType != null" + *ngIf="selectedProviderType != null && selectedProviderType !== providerType.WebAuthn" > {{ actionButtonText }} diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts index 7b60268b57d..16a95d6ba2f 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts @@ -39,6 +39,8 @@ import { import { CaptchaProtectedComponent } from "../captcha-protected.component"; import { TwoFactorAuthAuthenticatorComponent } from "./two-factor-auth-authenticator.component"; +import { TwoFactorAuthEmailComponent } from "./two-factor-auth-email.component"; +import { TwoFactorAuthWebAuthnComponent } from "./two-factor-auth-webauthn.component"; import { TwoFactorAuthYubikeyComponent } from "./two-factor-auth-yubikey.component"; import { TwoFactorOptionsDialogResult, @@ -60,7 +62,9 @@ import { ButtonModule, TwoFactorOptionsComponent, TwoFactorAuthAuthenticatorComponent, + TwoFactorAuthEmailComponent, TwoFactorAuthYubikeyComponent, + TwoFactorAuthWebAuthnComponent, ], providers: [I18nPipe], }) diff --git a/libs/angular/src/auth/components/two-factor.component.spec.ts b/libs/angular/src/auth/components/two-factor.component.spec.ts index 0eb248f6d9d..3325f3bc32d 100644 --- a/libs/angular/src/auth/components/two-factor.component.spec.ts +++ b/libs/angular/src/auth/components/two-factor.component.spec.ts @@ -32,6 +32,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; +import { ToastService } from "@bitwarden/components"; import { TwoFactorComponent } from "./two-factor.component"; @@ -71,6 +72,7 @@ describe("TwoFactorComponent", () => { let mockConfigService: MockProxy; let mockMasterPasswordService: FakeMasterPasswordService; let mockAccountService: FakeAccountService; + let mockToastService: MockProxy; let mockUserDecryptionOpts: { noMasterPassword: UserDecryptionOptions; @@ -102,6 +104,7 @@ describe("TwoFactorComponent", () => { mockSsoLoginService = mock(); mockConfigService = mock(); mockAccountService = mockAccountServiceWith(userId); + mockToastService = mock(); mockMasterPasswordService = new FakeMasterPasswordService(); mockUserDecryptionOpts = { @@ -182,6 +185,7 @@ describe("TwoFactorComponent", () => { { provide: ConfigService, useValue: mockConfigService }, { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, { provide: AccountService, useValue: mockAccountService }, + { provide: ToastService, useValue: mockToastService }, ], }); diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index d08e9a0a2ef..8c849db6c63 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -32,6 +32,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { ToastService } from "@bitwarden/components"; import { CaptchaProtectedComponent } from "./captcha-protected.component"; @@ -94,6 +95,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected configService: ConfigService, protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected accountService: AccountService, + protected toastService: ToastService, ) { super(environmentService, i18nService, platformUtilsService); this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); @@ -474,6 +476,15 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI } async launchDuoFrameless() { + if (this.duoFramelessUrl === null) { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("duoHealthCheckResultsInNullAuthUrlError"), + }); + return; + } + this.platformUtilsService.launchUri(this.duoFramelessUrl); } } diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts index 2ec13ea35e6..da8a4dd4181 100644 --- a/libs/angular/src/jslib.module.ts +++ b/libs/angular/src/jslib.module.ts @@ -46,6 +46,7 @@ import { StopClickDirective } from "./directives/stop-click.directive"; import { StopPropDirective } from "./directives/stop-prop.directive"; import { TrueFalseValueDirective } from "./directives/true-false-value.directive"; import { CreditCardNumberPipe } from "./pipes/credit-card-number.pipe"; +import { PluralizePipe } from "./pipes/pluralize.pipe"; import { SearchCiphersPipe } from "./pipes/search-ciphers.pipe"; import { SearchPipe } from "./pipes/search.pipe"; import { UserNamePipe } from "./pipes/user-name.pipe"; @@ -162,6 +163,7 @@ import { IconComponent } from "./vault/components/icon.component"; UserNamePipe, UserTypePipe, FingerprintPipe, + PluralizePipe, ], }) export class JslibModule {} diff --git a/libs/angular/src/pipes/pluralize.pipe.ts b/libs/angular/src/pipes/pluralize.pipe.ts new file mode 100644 index 00000000000..cc3aa3e0aa7 --- /dev/null +++ b/libs/angular/src/pipes/pluralize.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +@Pipe({ + name: "pluralize", + standalone: true, +}) +export class PluralizePipe implements PipeTransform { + transform(count: number, singular: string, plural: string): string { + return `${count} ${count === 1 ? singular : plural}`; + } +} diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 480acdd74d1..1b534b6f779 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -131,6 +131,7 @@ import { BraintreeService } from "@bitwarden/common/billing/services/payment-pro import { StripeService } from "@bitwarden/common/billing/services/payment-processors/stripe.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -157,11 +158,16 @@ import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/inter import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags"; import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; +import { + TaskSchedulerService, + DefaultTaskSchedulerService, +} from "@bitwarden/common/platform/scheduling"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; +import { BulkEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/bulk-encrypt.service.implementation"; import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; import { DefaultBroadcasterService } from "@bitwarden/common/platform/services/default-broadcaster.service"; import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; @@ -409,6 +415,7 @@ const safeProviders: SafeProvider[] = [ BillingAccountProfileStateService, VaultTimeoutSettingsServiceAbstraction, KdfConfigServiceAbstraction, + TaskSchedulerService, ], }), safeProvider({ @@ -432,6 +439,7 @@ const safeProviders: SafeProvider[] = [ stateService: StateServiceAbstraction, autofillSettingsService: AutofillSettingsServiceAbstraction, encryptService: EncryptService, + bulkEncryptService: BulkEncryptService, fileUploadService: CipherFileUploadServiceAbstraction, configService: ConfigService, stateProvider: StateProvider, @@ -445,6 +453,7 @@ const safeProviders: SafeProvider[] = [ stateService, autofillSettingsService, encryptService, + bulkEncryptService, fileUploadService, configService, stateProvider, @@ -458,6 +467,7 @@ const safeProviders: SafeProvider[] = [ StateServiceAbstraction, AutofillSettingsServiceAbstraction, EncryptService, + BulkEncryptService, CipherFileUploadServiceAbstraction, ConfigService, StateProvider, @@ -714,6 +724,8 @@ const safeProviders: SafeProvider[] = [ AuthServiceAbstraction, VaultTimeoutSettingsServiceAbstraction, StateEventRunnerService, + TaskSchedulerService, + LogService, LOCKED_CALLBACK, LOGOUT_CALLBACK, ], @@ -812,6 +824,7 @@ const safeProviders: SafeProvider[] = [ StateServiceAbstraction, AuthServiceAbstraction, MessagingServiceAbstraction, + TaskSchedulerService, ], }), safeProvider({ @@ -824,10 +837,21 @@ const safeProviders: SafeProvider[] = [ useClass: MultithreadEncryptServiceImplementation, deps: [CryptoFunctionServiceAbstraction, LogService, LOG_MAC_FAILURES], }), + safeProvider({ + provide: BulkEncryptService, + useClass: BulkEncryptServiceImplementation, + deps: [CryptoFunctionServiceAbstraction, LogService], + }), safeProvider({ provide: EventUploadServiceAbstraction, useClass: EventUploadService, - deps: [ApiServiceAbstraction, StateProvider, LogService, AuthServiceAbstraction], + deps: [ + ApiServiceAbstraction, + StateProvider, + LogService, + AuthServiceAbstraction, + TaskSchedulerService, + ], }), safeProvider({ provide: EventCollectionServiceAbstraction, @@ -954,7 +978,13 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: DefaultConfigService, useClass: DefaultConfigService, - deps: [ConfigApiServiceAbstraction, EnvironmentService, LogService, StateProvider], + deps: [ + ConfigApiServiceAbstraction, + EnvironmentService, + LogService, + StateProvider, + AuthServiceAbstraction, + ], }), safeProvider({ provide: ConfigService, @@ -1209,6 +1239,11 @@ const safeProviders: SafeProvider[] = [ new SubjectMessageSender(subject), deps: [INTRAPROCESS_MESSAGING_SUBJECT], }), + safeProvider({ + provide: TaskSchedulerService, + useClass: DefaultTaskSchedulerService, + deps: [LogService], + }), safeProvider({ provide: ProviderApiServiceAbstraction, useClass: ProviderApiService, diff --git a/libs/angular/src/services/unassigned-items-banner.api.service.ts b/libs/angular/src/services/unassigned-items-banner.api.service.ts deleted file mode 100644 index 69b74f8c7fa..00000000000 --- a/libs/angular/src/services/unassigned-items-banner.api.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from "@angular/core"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; - -@Injectable({ providedIn: "root" }) -export class UnassignedItemsBannerApiService { - constructor(private apiService: ApiService) {} - - async getShowUnassignedCiphersBanner(): Promise { - const r = await this.apiService.send( - "GET", - "/ciphers/has-unassigned-ciphers", - null, - true, - true, - ); - return r; - } -} diff --git a/libs/angular/src/services/unassigned-items-banner.service.spec.ts b/libs/angular/src/services/unassigned-items-banner.service.spec.ts deleted file mode 100644 index bf0fb23881c..00000000000 --- a/libs/angular/src/services/unassigned-items-banner.service.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { MockProxy, mock } from "jest-mock-extended"; -import { firstValueFrom, of } from "rxjs"; - -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec"; -import { UserId } from "@bitwarden/common/types/guid"; - -import { UnassignedItemsBannerApiService } from "./unassigned-items-banner.api.service"; -import { SHOW_BANNER_KEY, UnassignedItemsBannerService } from "./unassigned-items-banner.service"; - -describe("UnassignedItemsBanner", () => { - let stateProvider: FakeStateProvider; - let apiService: MockProxy; - let environmentService: MockProxy; - let organizationService: MockProxy; - - const sutFactory = () => - new UnassignedItemsBannerService( - stateProvider, - apiService, - environmentService, - organizationService, - ); - - beforeEach(() => { - const fakeAccountService = mockAccountServiceWith("userId" as UserId); - stateProvider = new FakeStateProvider(fakeAccountService); - apiService = mock(); - environmentService = mock(); - environmentService.environment$ = of(null); - organizationService = mock(); - organizationService.organizations$ = of([]); - }); - - it("shows the banner if showBanner local state is true", async () => { - const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY); - showBanner.nextState(true); - - const sut = sutFactory(); - expect(await firstValueFrom(sut.showBanner$)).toBe(true); - expect(apiService.getShowUnassignedCiphersBanner).not.toHaveBeenCalled(); - }); - - it("does not show the banner if showBanner local state is false", async () => { - const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY); - showBanner.nextState(false); - - const sut = sutFactory(); - expect(await firstValueFrom(sut.showBanner$)).toBe(false); - expect(apiService.getShowUnassignedCiphersBanner).not.toHaveBeenCalled(); - }); - - it("fetches from server if local state has not been set yet", async () => { - apiService.getShowUnassignedCiphersBanner.mockResolvedValue(true); - - const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY); - showBanner.nextState(undefined); - - const sut = sutFactory(); - - expect(await firstValueFrom(sut.showBanner$)).toBe(true); - expect(apiService.getShowUnassignedCiphersBanner).toHaveBeenCalledTimes(1); - }); -}); diff --git a/libs/angular/src/services/unassigned-items-banner.service.ts b/libs/angular/src/services/unassigned-items-banner.service.ts deleted file mode 100644 index db93d4c4fca..00000000000 --- a/libs/angular/src/services/unassigned-items-banner.service.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Injectable } from "@angular/core"; -import { combineLatest, concatMap, map, startWith } from "rxjs"; - -import { - OrganizationService, - canAccessOrgAdmin, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { - EnvironmentService, - Region, -} from "@bitwarden/common/platform/abstractions/environment.service"; -import { - StateProvider, - UNASSIGNED_ITEMS_BANNER_DISK, - UserKeyDefinition, -} from "@bitwarden/common/platform/state"; - -import { UnassignedItemsBannerApiService } from "./unassigned-items-banner.api.service"; - -export const SHOW_BANNER_KEY = new UserKeyDefinition( - UNASSIGNED_ITEMS_BANNER_DISK, - "showBanner", - { - deserializer: (b) => b, - clearOn: [], - }, -); - -/** Displays a banner that tells users how to move their unassigned items into a collection. */ -@Injectable({ providedIn: "root" }) -export class UnassignedItemsBannerService { - private _showBanner = this.stateProvider.getActive(SHOW_BANNER_KEY); - - showBanner$ = this._showBanner.state$.pipe( - concatMap(async (showBannerState) => { - // null indicates that the user has not seen or dismissed the banner yet - get the flag from server - if (showBannerState == null) { - const showBannerResponse = await this.apiService.getShowUnassignedCiphersBanner(); - await this._showBanner.update(() => showBannerResponse); - return showBannerResponse; - } - - return showBannerState; - }), - ); - - private adminConsoleOrg$ = this.organizationService.organizations$.pipe( - map((orgs) => orgs.find((o) => canAccessOrgAdmin(o))), - ); - - adminConsoleUrl$ = combineLatest([ - this.adminConsoleOrg$, - this.environmentService.environment$, - ]).pipe( - map(([org, environment]) => { - if (org == null || environment == null) { - return "#"; - } - - return environment.getWebVaultUrl() + "/#/organizations/" + org.id; - }), - ); - - bannerText$ = this.environmentService.environment$.pipe( - map((e) => - e?.getRegion() == Region.SelfHosted - ? "unassignedItemsBannerSelfHostNotice" - : "unassignedItemsBannerNotice", - ), - ); - - loading$ = combineLatest([this.adminConsoleUrl$, this.bannerText$]).pipe( - startWith(true), - map(() => false), - ); - - constructor( - private stateProvider: StateProvider, - private apiService: UnassignedItemsBannerApiService, - private environmentService: EnvironmentService, - private organizationService: OrganizationService, - ) {} - - async hideBanner() { - await this._showBanner.update(() => false); - } -} diff --git a/libs/angular/src/vault/components/attachments.component.ts b/libs/angular/src/vault/components/attachments.component.ts index fc86f2f5277..68b336a8b06 100644 --- a/libs/angular/src/vault/components/attachments.component.ts +++ b/libs/angular/src/vault/components/attachments.component.ts @@ -20,6 +20,7 @@ import { DialogService } from "@bitwarden/components"; @Directive() export class AttachmentsComponent implements OnInit { @Input() cipherId: string; + @Input() viewOnly: boolean; @Output() onUploadedAttachment = new EventEmitter(); @Output() onDeletedAttachment = new EventEmitter(); @Output() onReuploadedAttachment = new EventEmitter(); diff --git a/libs/auth/src/angular/input-password/input-password.component.html b/libs/auth/src/angular/input-password/input-password.component.html index 5cfcf671655..42d67a77c9a 100644 --- a/libs/auth/src/angular/input-password/input-password.component.html +++ b/libs/auth/src/angular/input-password/input-password.component.html @@ -27,12 +27,12 @@ - + [email]="email" + [password]="formGroup.controls.password.value" + (passwordStrengthScore)="getPasswordStrengthScore($event)" + >
diff --git a/libs/auth/src/angular/input-password/input-password.component.ts b/libs/auth/src/angular/input-password/input-password.component.ts index ed77e17da1a..7b5651492e1 100644 --- a/libs/auth/src/angular/input-password/input-password.component.ts +++ b/libs/auth/src/angular/input-password/input-password.component.ts @@ -2,6 +2,10 @@ import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { ReactiveFormsModule, FormBuilder, Validators } from "@angular/forms"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { + PasswordStrengthScore, + PasswordStrengthV2Component, +} from "@bitwarden/angular/tools/password-strength/password-strength-v2.component"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; @@ -40,6 +44,7 @@ import { PasswordInputResult } from "./password-input-result"; ReactiveFormsModule, SharedModule, PasswordCalloutComponent, + PasswordStrengthV2Component, JslibModule, ], }) @@ -56,7 +61,7 @@ export class InputPasswordComponent implements OnInit { protected minPasswordLength = Utils.minimumPasswordLength; protected minPasswordMsg = ""; - protected passwordStrengthResult: any; + protected passwordStrengthScore: PasswordStrengthScore; protected showErrorSummary = false; protected showPassword = false; @@ -112,8 +117,8 @@ export class InputPasswordComponent implements OnInit { } } - getPasswordStrengthResult(result: any) { - this.passwordStrengthResult = result; + getPasswordStrengthScore(score: PasswordStrengthScore) { + this.passwordStrengthScore = score; } protected submit = async () => { @@ -147,7 +152,7 @@ export class InputPasswordComponent implements OnInit { if ( this.masterPasswordPolicyOptions != null && !this.policyService.evaluateMasterPassword( - this.passwordStrengthResult.score, + this.passwordStrengthScore, password, this.masterPasswordPolicyOptions, ) diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.html b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.html index 217a7745ebf..70ca948f93d 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.html +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.html @@ -8,4 +8,5 @@ [masterPasswordPolicyOptions]="masterPasswordPolicyOptions" [loading]="submitting" (onPasswordFormSubmit)="handlePasswordFormSubmit($event)" + [buttonText]="'createAccount' | i18n" > diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts index f0a8d81bea8..778ad7c74c2 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts @@ -27,6 +27,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { KdfType } from "@bitwarden/common/platform/enums"; +import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling"; import { FakeAccountService, FakeGlobalState, @@ -72,6 +73,7 @@ describe("LoginStrategyService", () => { let billingAccountProfileStateService: MockProxy; let vaultTimeoutSettingsService: MockProxy; let kdfConfigService: MockProxy; + let taskSchedulerService: MockProxy; let stateProvider: FakeGlobalStateProvider; let loginStrategyCacheExpirationState: FakeGlobalState; @@ -103,6 +105,7 @@ describe("LoginStrategyService", () => { stateProvider = new FakeGlobalStateProvider(); vaultTimeoutSettingsService = mock(); kdfConfigService = mock(); + taskSchedulerService = mock(); sut = new LoginStrategyService( accountService, @@ -129,6 +132,7 @@ describe("LoginStrategyService", () => { billingAccountProfileStateService, vaultTimeoutSettingsService, kdfConfigService, + taskSchedulerService, ); loginStrategyCacheExpirationState = stateProvider.getFake(CACHE_EXPIRATION_KEY); diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 7169fd69e93..67bcdc3658e 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -5,6 +5,7 @@ import { map, Observable, shareReplay, + Subscription, } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -37,6 +38,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { KdfType } from "@bitwarden/common/platform/enums/kdf-type.enum"; +import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; @@ -69,7 +71,7 @@ import { const sessionTimeoutLength = 2 * 60 * 1000; // 2 minutes export class LoginStrategyService implements LoginStrategyServiceAbstraction { - private sessionTimeout: unknown; + private sessionTimeoutSubscription: Subscription; private currentAuthnTypeState: GlobalState; private loginStrategyCacheState: GlobalState; private loginStrategyCacheExpirationState: GlobalState; @@ -111,6 +113,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { protected billingAccountProfileStateService: BillingAccountProfileStateService, protected vaultTimeoutSettingsService: VaultTimeoutSettingsService, protected kdfConfigService: KdfConfigService, + protected taskSchedulerService: TaskSchedulerService, ) { this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY); this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY); @@ -118,6 +121,10 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.authRequestPushNotificationState = this.stateProvider.get( AUTH_REQUEST_PUSH_NOTIFICATION_KEY, ); + this.taskSchedulerService.registerTaskHandler( + ScheduledTaskNames.loginStrategySessionTimeout, + () => this.clearCache(), + ); this.currentAuthType$ = this.currentAuthnTypeState.state$; this.loginStrategy$ = this.currentAuthnTypeState.state$.pipe( @@ -268,15 +275,23 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { private async startSessionTimeout(): Promise { await this.clearSessionTimeout(); + + // This Login Strategy Cache Expiration State value set here is used to clear the cache on re-init + // of the application in the case where the timeout is terminated due to a closure of the application + // window. The browser extension popup in particular is susceptible to this concern, as the user + // is almost always likely to close the popup window before the session timeout is reached. await this.loginStrategyCacheExpirationState.update( (_) => new Date(Date.now() + sessionTimeoutLength), ); - this.sessionTimeout = setTimeout(() => this.clearCache(), sessionTimeoutLength); + this.sessionTimeoutSubscription = this.taskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + sessionTimeoutLength, + ); } private async clearSessionTimeout(): Promise { await this.loginStrategyCacheExpirationState.update((_) => null); - this.sessionTimeout = null; + this.sessionTimeoutSubscription?.unsubscribe(); } private async isSessionValid(): Promise { @@ -284,6 +299,9 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { if (cache == null) { return false; } + + // If the Login Strategy Cache Expiration State value is less than the current + // datetime stamp, then the cache is invalid and should be cleared. const expiration = await firstValueFrom(this.loginStrategyCacheExpirationState.state$); if (expiration != null && expiration < new Date()) { await this.clearCache(); diff --git a/libs/common/src/admin-console/enums/organization-user-type.enum.ts b/libs/common/src/admin-console/enums/organization-user-type.enum.ts index 657d2a4a6cb..da50bfbdc20 100644 --- a/libs/common/src/admin-console/enums/organization-user-type.enum.ts +++ b/libs/common/src/admin-console/enums/organization-user-type.enum.ts @@ -2,10 +2,6 @@ export enum OrganizationUserType { Owner = 0, Admin = 1, User = 2, - /** - * @deprecated - * This is deprecated with the introduction of Flexible Collections. - */ - Manager = 3, + // Manager = 3 has been intentionally permanently deleted Custom = 4, } diff --git a/libs/common/src/autofill/constants/index.ts b/libs/common/src/autofill/constants/index.ts index 6d5af41a17e..efbd0896428 100644 --- a/libs/common/src/autofill/constants/index.ts +++ b/libs/common/src/autofill/constants/index.ts @@ -13,13 +13,16 @@ export const EVENTS = { BLUR: "blur", CLICK: "click", FOCUS: "focus", + FOCUSIN: "focusin", + FOCUSOUT: "focusout", SCROLL: "scroll", RESIZE: "resize", DOMCONTENTLOADED: "DOMContentLoaded", LOAD: "load", MESSAGE: "message", VISIBILITYCHANGE: "visibilitychange", - FOCUSOUT: "focusout", + MOUSEENTER: "mouseenter", + MOUSELEAVE: "mouseleave", } as const; export const ClearClipboardDelay = { @@ -51,6 +54,8 @@ export const SEPARATOR_ID = "separator"; export const NOTIFICATION_BAR_LIFESPAN_MS = 150000; // 150 seconds +export const AUTOFILL_OVERLAY_HANDLE_REPOSITION = "autofill-overlay-handle-reposition-event"; + export const AutofillOverlayVisibility = { Off: 0, OnButtonClick: 1, diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index de387480f7e..7e88af236fa 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -11,18 +11,21 @@ export enum FeatureFlag { ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners", EnableConsolidatedBilling = "enable-consolidated-billing", AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section", - UnassignedItemsBanner = "unassigned-items-banner", EnableDeleteProvider = "AC-1218-delete-provider", ExtensionRefresh = "extension-refresh", RestrictProviderAccess = "restrict-provider-access", + PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service", UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection", EmailVerification = "email-verification", InlineMenuFieldQualification = "inline-menu-field-qualification", MemberAccessReport = "ac-2059-member-access-report", TwoFactorComponentRefactor = "two-factor-component-refactor", EnableTimeThreshold = "PM-5864-dollar-threshold", + InlineMenuPositioningImprovements = "inline-menu-positioning-improvements", GroupsComponentRefactor = "groups-component-refactor", ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner", + VaultBulkManagementAction = "vault-bulk-management-action", + AC2828_ProviderPortalMembersPage = "AC-2828_provider-portal-members-page", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -44,18 +47,21 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.ShowPaymentMethodWarningBanners]: FALSE, [FeatureFlag.EnableConsolidatedBilling]: FALSE, [FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE, - [FeatureFlag.UnassignedItemsBanner]: FALSE, [FeatureFlag.EnableDeleteProvider]: FALSE, [FeatureFlag.ExtensionRefresh]: FALSE, [FeatureFlag.RestrictProviderAccess]: FALSE, + [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, [FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE, [FeatureFlag.EmailVerification]: FALSE, [FeatureFlag.InlineMenuFieldQualification]: FALSE, [FeatureFlag.MemberAccessReport]: FALSE, [FeatureFlag.TwoFactorComponentRefactor]: FALSE, [FeatureFlag.EnableTimeThreshold]: FALSE, + [FeatureFlag.InlineMenuPositioningImprovements]: FALSE, [FeatureFlag.GroupsComponentRefactor]: FALSE, [FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE, + [FeatureFlag.VaultBulkManagementAction]: FALSE, + [FeatureFlag.AC2828_ProviderPortalMembersPage]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; diff --git a/libs/common/src/platform/abstractions/bulk-encrypt.service.ts b/libs/common/src/platform/abstractions/bulk-encrypt.service.ts new file mode 100644 index 00000000000..4cdff0c769a --- /dev/null +++ b/libs/common/src/platform/abstractions/bulk-encrypt.service.ts @@ -0,0 +1,10 @@ +import { Decryptable } from "../interfaces/decryptable.interface"; +import { InitializerMetadata } from "../interfaces/initializer-metadata.interface"; +import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; + +export abstract class BulkEncryptService { + abstract decryptItems( + items: Decryptable[], + key: SymmetricCryptoKey, + ): Promise; +} diff --git a/libs/common/src/platform/abstractions/encrypt.service.ts b/libs/common/src/platform/abstractions/encrypt.service.ts index bc526e35784..c70042e4186 100644 --- a/libs/common/src/platform/abstractions/encrypt.service.ts +++ b/libs/common/src/platform/abstractions/encrypt.service.ts @@ -13,6 +13,11 @@ export abstract class EncryptService { abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise; abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise; abstract resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey; + /** + * @deprecated Replaced by BulkEncryptService, remove once the feature is tested and the featureflag PM-4154-multi-worker-encryption-service is removed + * @param items The items to decrypt + * @param key The key to decrypt the items with + */ abstract decryptItems( items: Decryptable[], key: SymmetricCryptoKey, diff --git a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts index aba18f9ecd1..9882febdd39 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts @@ -12,6 +12,11 @@ export interface NewCredentialParams { */ userName: string; + /** + * The userhandle (userid) of the user. + */ + userHandle: string; + /** * Whether or not the user must be verified before completing the operation. */ diff --git a/libs/common/src/platform/scheduling/default-task-scheduler.service.spec.ts b/libs/common/src/platform/scheduling/default-task-scheduler.service.spec.ts new file mode 100644 index 00000000000..ec66947f0eb --- /dev/null +++ b/libs/common/src/platform/scheduling/default-task-scheduler.service.spec.ts @@ -0,0 +1,123 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { LogService } from "../abstractions/log.service"; +import { ScheduledTaskNames } from "../scheduling/scheduled-task-name.enum"; + +import { DefaultTaskSchedulerService } from "./default-task-scheduler.service"; + +describe("DefaultTaskSchedulerService", () => { + const callback = jest.fn(); + const delayInMs = 1000; + const intervalInMs = 1100; + let logService: MockProxy; + let taskSchedulerService: DefaultTaskSchedulerService; + + beforeEach(() => { + jest.useFakeTimers(); + logService = mock(); + taskSchedulerService = new DefaultTaskSchedulerService(logService); + taskSchedulerService.registerTaskHandler( + ScheduledTaskNames.loginStrategySessionTimeout, + callback, + ); + }); + + afterEach(() => { + jest.clearAllTimers(); + jest.clearAllMocks(); + }); + + it("triggers an error when setting a timeout for a task that is not registered", async () => { + expect(() => + taskSchedulerService.setTimeout(ScheduledTaskNames.notificationsReconnectTimeout, 1000), + ).toThrow( + `Task handler for ${ScheduledTaskNames.notificationsReconnectTimeout} not registered. Unable to schedule task.`, + ); + }); + + it("triggers an error when setting an interval for a task that is not registered", async () => { + expect(() => + taskSchedulerService.setInterval(ScheduledTaskNames.notificationsReconnectTimeout, 1000), + ).toThrow( + `Task handler for ${ScheduledTaskNames.notificationsReconnectTimeout} not registered. Unable to schedule task.`, + ); + }); + + it("overrides the handler for a previously registered task and provides a warning about the task registration", () => { + taskSchedulerService.registerTaskHandler( + ScheduledTaskNames.loginStrategySessionTimeout, + callback, + ); + + expect(logService.warning).toHaveBeenCalledWith( + `Task handler for ${ScheduledTaskNames.loginStrategySessionTimeout} already exists. Overwriting.`, + ); + expect( + taskSchedulerService["taskHandlers"].get(ScheduledTaskNames.loginStrategySessionTimeout), + ).toBeDefined(); + }); + + it("sets a timeout and returns the timeout id", () => { + const timeoutId = taskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs, + ); + + expect(timeoutId).toBeDefined(); + expect(callback).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(delayInMs); + + expect(callback).toHaveBeenCalled(); + }); + + it("sets an interval timeout and results the interval id", () => { + const intervalId = taskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + intervalInMs, + ); + + expect(intervalId).toBeDefined(); + expect(callback).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(intervalInMs); + + expect(callback).toHaveBeenCalled(); + + jest.advanceTimersByTime(intervalInMs); + + expect(callback).toHaveBeenCalledTimes(2); + }); + + it("clears scheduled tasks using the timeout id", () => { + const timeoutHandle = taskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs, + ); + + expect(timeoutHandle).toBeDefined(); + expect(callback).not.toHaveBeenCalled(); + + timeoutHandle.unsubscribe(); + + jest.advanceTimersByTime(delayInMs); + + expect(callback).not.toHaveBeenCalled(); + }); + + it("clears scheduled tasks using the interval id", () => { + const intervalHandle = taskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + intervalInMs, + ); + + expect(intervalHandle).toBeDefined(); + expect(callback).not.toHaveBeenCalled(); + + intervalHandle.unsubscribe(); + + jest.advanceTimersByTime(intervalInMs); + + expect(callback).not.toHaveBeenCalled(); + }); +}); diff --git a/libs/common/src/platform/scheduling/default-task-scheduler.service.ts b/libs/common/src/platform/scheduling/default-task-scheduler.service.ts new file mode 100644 index 00000000000..4de2faec644 --- /dev/null +++ b/libs/common/src/platform/scheduling/default-task-scheduler.service.ts @@ -0,0 +1,97 @@ +import { Subscription } from "rxjs"; + +import { LogService } from "../abstractions/log.service"; +import { ScheduledTaskName } from "../scheduling/scheduled-task-name.enum"; +import { TaskSchedulerService } from "../scheduling/task-scheduler.service"; + +export class DefaultTaskSchedulerService extends TaskSchedulerService { + constructor(protected logService: LogService) { + super(); + + this.taskHandlers = new Map(); + } + + /** + * Sets a timeout and returns the timeout id. + * + * @param taskName - The name of the task. Unused in the base implementation. + * @param delayInMs - The delay in milliseconds. + */ + setTimeout(taskName: ScheduledTaskName, delayInMs: number): Subscription { + this.validateRegisteredTask(taskName); + + const timeoutHandle = globalThis.setTimeout(() => this.triggerTask(taskName), delayInMs); + return new Subscription(() => globalThis.clearTimeout(timeoutHandle)); + } + + /** + * Sets an interval and returns the interval id. + * + * @param taskName - The name of the task. Unused in the base implementation. + * @param intervalInMs - The interval in milliseconds. + * @param _initialDelayInMs - The initial delay in milliseconds. Unused in the base implementation. + */ + setInterval( + taskName: ScheduledTaskName, + intervalInMs: number, + _initialDelayInMs?: number, + ): Subscription { + this.validateRegisteredTask(taskName); + + const intervalHandle = globalThis.setInterval(() => this.triggerTask(taskName), intervalInMs); + + return new Subscription(() => globalThis.clearInterval(intervalHandle)); + } + + /** + * Registers a task handler. + * + * @param taskName - The name of the task. + * @param handler - The task handler. + */ + registerTaskHandler(taskName: ScheduledTaskName, handler: () => void) { + const existingHandler = this.taskHandlers.get(taskName); + if (existingHandler) { + this.logService.warning(`Task handler for ${taskName} already exists. Overwriting.`); + this.unregisterTaskHandler(taskName); + } + + this.taskHandlers.set(taskName, handler); + } + + /** + * Unregisters a task handler. + * + * @param taskName - The name of the task. + */ + unregisterTaskHandler(taskName: ScheduledTaskName) { + this.taskHandlers.delete(taskName); + } + + /** + * Triggers a task. + * + * @param taskName - The name of the task. + * @param _periodInMinutes - The period in minutes. Unused in the base implementation. + */ + protected async triggerTask( + taskName: ScheduledTaskName, + _periodInMinutes?: number, + ): Promise { + const handler = this.taskHandlers.get(taskName); + if (handler) { + handler(); + } + } + + /** + * Validates that a task handler is registered. + * + * @param taskName - The name of the task. + */ + protected validateRegisteredTask(taskName: ScheduledTaskName): void { + if (!this.taskHandlers.has(taskName)) { + throw new Error(`Task handler for ${taskName} not registered. Unable to schedule task.`); + } + } +} diff --git a/libs/common/src/platform/scheduling/index.ts b/libs/common/src/platform/scheduling/index.ts new file mode 100644 index 00000000000..e5f10ca3baf --- /dev/null +++ b/libs/common/src/platform/scheduling/index.ts @@ -0,0 +1,3 @@ +export { TaskSchedulerService } from "./task-scheduler.service"; +export { DefaultTaskSchedulerService } from "./default-task-scheduler.service"; +export { ScheduledTaskNames, ScheduledTaskName } from "./scheduled-task-name.enum"; diff --git a/libs/common/src/platform/scheduling/scheduled-task-name.enum.ts b/libs/common/src/platform/scheduling/scheduled-task-name.enum.ts new file mode 100644 index 00000000000..2c0ffc87eb7 --- /dev/null +++ b/libs/common/src/platform/scheduling/scheduled-task-name.enum.ts @@ -0,0 +1,12 @@ +export const ScheduledTaskNames = { + generatePasswordClearClipboardTimeout: "generatePasswordClearClipboardTimeout", + systemClearClipboardTimeout: "systemClearClipboardTimeout", + loginStrategySessionTimeout: "loginStrategySessionTimeout", + notificationsReconnectTimeout: "notificationsReconnectTimeout", + fido2ClientAbortTimeout: "fido2ClientAbortTimeout", + scheduleNextSyncInterval: "scheduleNextSyncInterval", + eventUploadsInterval: "eventUploadsInterval", + vaultTimeoutCheckInterval: "vaultTimeoutCheckInterval", +} as const; + +export type ScheduledTaskName = (typeof ScheduledTaskNames)[keyof typeof ScheduledTaskNames]; diff --git a/libs/common/src/platform/scheduling/task-scheduler.service.ts b/libs/common/src/platform/scheduling/task-scheduler.service.ts new file mode 100644 index 00000000000..57e5291f7c6 --- /dev/null +++ b/libs/common/src/platform/scheduling/task-scheduler.service.ts @@ -0,0 +1,16 @@ +import { Subscription } from "rxjs"; + +import { ScheduledTaskName } from "./scheduled-task-name.enum"; + +export abstract class TaskSchedulerService { + protected taskHandlers: Map void>; + abstract setTimeout(taskName: ScheduledTaskName, delayInMs: number): Subscription; + abstract setInterval( + taskName: ScheduledTaskName, + intervalInMs: number, + initialDelayInMs?: number, + ): Subscription; + abstract registerTaskHandler(taskName: ScheduledTaskName, handler: () => void): void; + abstract unregisterTaskHandler(taskName: ScheduledTaskName): void; + protected abstract triggerTask(taskName: ScheduledTaskName, periodInMinutes?: number): void; +} diff --git a/libs/common/src/platform/services/config/config.service.spec.ts b/libs/common/src/platform/services/config/config.service.spec.ts index d643311a26f..d7e33473d01 100644 --- a/libs/common/src/platform/services/config/config.service.spec.ts +++ b/libs/common/src/platform/services/config/config.service.spec.ts @@ -14,6 +14,8 @@ import { mockAccountServiceWith, } from "../../../../spec"; import { subscribeTo } from "../../../../spec/observable-tracker"; +import { AuthService } from "../../../auth/abstractions/auth.service"; +import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { UserId } from "../../../types/guid"; import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction"; import { ServerConfig } from "../../abstractions/config/server-config"; @@ -39,6 +41,9 @@ describe("ConfigService", () => { const configApiService = mock(); const environmentService = mock(); const logService = mock(); + const authService = mock({ + authStatusFor$: (userId) => of(AuthenticationStatus.Unlocked), + }); let stateProvider: FakeStateProvider; let globalState: FakeGlobalState>; let userState: FakeSingleUserState; @@ -71,6 +76,7 @@ describe("ConfigService", () => { environmentService, logService, stateProvider, + authService, ); }); @@ -188,6 +194,30 @@ describe("ConfigService", () => { }); }); + it("gets global config when there is an locked active user", async () => { + await accountService.switchAccount(userId); + environmentService.environment$ = of(environmentFactory(activeApiUrl)); + + globalState.stateSubject.next({ + [activeApiUrl]: serverConfigFactory(activeApiUrl + "global"), + }); + userState.nextState(serverConfigFactory(userId)); + + const sut = new DefaultConfigService( + configApiService, + environmentService, + logService, + stateProvider, + mock({ + authStatusFor$: () => of(AuthenticationStatus.Locked), + }), + ); + + const config = await firstValueFrom(sut.serverConfig$); + + expect(config.gitHash).toEqual(activeApiUrl + "global"); + }); + describe("environment change", () => { let sut: DefaultConfigService; let environmentSubject: Subject; @@ -205,6 +235,7 @@ describe("ConfigService", () => { environmentService, logService, stateProvider, + authService, ); }); diff --git a/libs/common/src/platform/services/config/default-config.service.ts b/libs/common/src/platform/services/config/default-config.service.ts index 0a306348d7b..16878a72832 100644 --- a/libs/common/src/platform/services/config/default-config.service.ts +++ b/libs/common/src/platform/services/config/default-config.service.ts @@ -13,6 +13,8 @@ import { } from "rxjs"; import { SemVer } from "semver"; +import { AuthService } from "../../../auth/abstractions/auth.service"; +import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { DefaultFeatureFlagValue, FeatureFlag, @@ -60,16 +62,25 @@ export class DefaultConfigService implements ConfigService { private environmentService: EnvironmentService, private logService: LogService, private stateProvider: StateProvider, + private authService: AuthService, ) { const apiUrl$ = this.environmentService.environment$.pipe( map((environment) => environment.getApiUrl()), ); + const userId$ = this.stateProvider.activeUserId$; + const authStatus$ = userId$.pipe( + switchMap((userId) => (userId == null ? of(null) : this.authService.authStatusFor$(userId))), + ); - this.serverConfig$ = combineLatest([this.stateProvider.activeUserId$, apiUrl$]).pipe( - switchMap(([userId, apiUrl]) => { - const config$ = - userId == null ? this.globalConfigFor$(apiUrl) : this.userConfigFor$(userId); - return config$.pipe(map((config) => [config, userId, apiUrl] as const)); + this.serverConfig$ = combineLatest([userId$, apiUrl$, authStatus$]).pipe( + switchMap(([userId, apiUrl, authStatus]) => { + if (userId == null || authStatus !== AuthenticationStatus.Unlocked) { + return this.globalConfigFor$(apiUrl).pipe( + map((config) => [config, null, apiUrl] as const), + ); + } + + return this.userConfigFor$(userId).pipe(map((config) => [config, userId, apiUrl] as const)); }), tap(async (rec) => { const [existingConfig, userId, apiUrl] = rec; diff --git a/libs/common/src/platform/services/cryptography/bulk-encrypt.service.implementation.ts b/libs/common/src/platform/services/cryptography/bulk-encrypt.service.implementation.ts new file mode 100644 index 00000000000..d3bbc82905a --- /dev/null +++ b/libs/common/src/platform/services/cryptography/bulk-encrypt.service.implementation.ts @@ -0,0 +1,164 @@ +import { firstValueFrom, fromEvent, filter, map, takeUntil, defaultIfEmpty, Subject } from "rxjs"; +import { Jsonify } from "type-fest"; + +import { BulkEncryptService } from "../../abstractions/bulk-encrypt.service"; +import { CryptoFunctionService } from "../../abstractions/crypto-function.service"; +import { LogService } from "../../abstractions/log.service"; +import { Decryptable } from "../../interfaces/decryptable.interface"; +import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface"; +import { Utils } from "../../misc/utils"; +import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; + +import { getClassInitializer } from "./get-class-initializer"; + +// TTL (time to live) is not strictly required but avoids tying up memory resources if inactive +const workerTTL = 60000; // 1 minute +const maxWorkers = 8; +const minNumberOfItemsForMultithreading = 400; + +export class BulkEncryptServiceImplementation implements BulkEncryptService { + private workers: Worker[] = []; + private timeout: any; + + private clear$ = new Subject(); + + constructor( + protected cryptoFunctionService: CryptoFunctionService, + protected logService: LogService, + ) {} + + /** + * Decrypts items using a web worker if the environment supports it. + * Will fall back to the main thread if the window object is not available. + */ + async decryptItems( + items: Decryptable[], + key: SymmetricCryptoKey, + ): Promise { + if (key == null) { + throw new Error("No encryption key provided."); + } + + if (items == null || items.length < 1) { + return []; + } + + if (typeof window === "undefined") { + this.logService.info("Window not available in BulkEncryptService, decrypting sequentially"); + const results = []; + for (let i = 0; i < items.length; i++) { + results.push(await items[i].decrypt(key)); + } + return results; + } + + const decryptedItems = await this.getDecryptedItemsFromWorkers(items, key); + return decryptedItems; + } + + /** + * Sends items to a set of web workers to decrypt them. This utilizes multiple workers to decrypt items + * faster without interrupting other operations (e.g. updating UI). + */ + private async getDecryptedItemsFromWorkers( + items: Decryptable[], + key: SymmetricCryptoKey, + ): Promise { + if (items == null || items.length < 1) { + return []; + } + + this.clearTimeout(); + + const hardwareConcurrency = navigator.hardwareConcurrency || 1; + let numberOfWorkers = Math.min(hardwareConcurrency, maxWorkers); + if (items.length < minNumberOfItemsForMultithreading) { + numberOfWorkers = 1; + } + + this.logService.info( + `Starting decryption using multithreading with ${numberOfWorkers} workers for ${items.length} items`, + ); + + if (this.workers.length == 0) { + for (let i = 0; i < numberOfWorkers; i++) { + this.workers.push( + new Worker( + new URL( + /* webpackChunkName: 'encrypt-worker' */ + "@bitwarden/common/platform/services/cryptography/encrypt.worker.ts", + import.meta.url, + ), + ), + ); + } + } + + const itemsPerWorker = Math.floor(items.length / this.workers.length); + const results = []; + + for (const [i, worker] of this.workers.entries()) { + const start = i * itemsPerWorker; + const end = start + itemsPerWorker; + const itemsForWorker = items.slice(start, end); + + // push the remaining items to the last worker + if (i == this.workers.length - 1) { + itemsForWorker.push(...items.slice(end)); + } + + const request = { + id: Utils.newGuid(), + items: itemsForWorker, + key: key, + }; + + worker.postMessage(JSON.stringify(request)); + results.push( + firstValueFrom( + fromEvent(worker, "message").pipe( + filter((response: MessageEvent) => response.data?.id === request.id), + map((response) => JSON.parse(response.data.items)), + map((items) => + items.map((jsonItem: Jsonify) => { + const initializer = getClassInitializer(jsonItem.initializerKey); + return initializer(jsonItem); + }), + ), + takeUntil(this.clear$), + defaultIfEmpty([]), + ), + ), + ); + } + + const decryptedItems = (await Promise.all(results)).flat(); + this.logService.info( + `Finished decrypting ${decryptedItems.length} items using ${numberOfWorkers} workers`, + ); + + this.restartTimeout(); + + return decryptedItems; + } + + private clear() { + this.clear$.next(); + for (const worker of this.workers) { + worker.terminate(); + } + this.workers = []; + this.clearTimeout(); + } + + private restartTimeout() { + this.clearTimeout(); + this.timeout = setTimeout(() => this.clear(), workerTTL); + } + + private clearTimeout() { + if (this.timeout != null) { + clearTimeout(this.timeout); + } + } +} diff --git a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts b/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts index 862ae2bc0e6..228f0c54174 100644 --- a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts +++ b/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts @@ -185,6 +185,9 @@ export class EncryptServiceImplementation implements EncryptService { return this.cryptoFunctionService.rsaDecrypt(data.dataBytes, privateKey, algorithm); } + /** + * @deprecated Replaced by BulkEncryptService (PM-4154) + */ async decryptItems( items: Decryptable[], key: SymmetricCryptoKey, diff --git a/libs/common/src/platform/services/cryptography/fallback-bulk-encrypt.service.ts b/libs/common/src/platform/services/cryptography/fallback-bulk-encrypt.service.ts new file mode 100644 index 00000000000..44dc5a8bf76 --- /dev/null +++ b/libs/common/src/platform/services/cryptography/fallback-bulk-encrypt.service.ts @@ -0,0 +1,33 @@ +import { BulkEncryptService } from "../../abstractions/bulk-encrypt.service"; +import { EncryptService } from "../../abstractions/encrypt.service"; +import { Decryptable } from "../../interfaces/decryptable.interface"; +import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface"; +import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; + +/** + * @deprecated For the feature flag from PM-4154, remove once feature is rolled out + */ +export class FallbackBulkEncryptService implements BulkEncryptService { + private featureFlagEncryptService: BulkEncryptService; + + constructor(protected encryptService: EncryptService) {} + + /** + * Decrypts items using a web worker if the environment supports it. + * Will fall back to the main thread if the window object is not available. + */ + async decryptItems( + items: Decryptable[], + key: SymmetricCryptoKey, + ): Promise { + if (this.featureFlagEncryptService != null) { + return await this.featureFlagEncryptService.decryptItems(items, key); + } else { + return await this.encryptService.decryptItems(items, key); + } + } + + async setFeatureFlagEncryptService(featureFlagEncryptService: BulkEncryptService) { + this.featureFlagEncryptService = featureFlagEncryptService; + } +} diff --git a/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts b/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts index 6ac343bcb6a..227db77526a 100644 --- a/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts +++ b/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts @@ -12,6 +12,9 @@ import { getClassInitializer } from "./get-class-initializer"; // TTL (time to live) is not strictly required but avoids tying up memory resources if inactive const workerTTL = 3 * 60000; // 3 minutes +/** + * @deprecated Replaced by BulkEncryptionService (PM-4154) + */ export class MultithreadEncryptServiceImplementation extends EncryptServiceImplementation { private worker: Worker; private timeout: any; diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts index 5da67f807b7..202381c5ead 100644 --- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts +++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts @@ -215,6 +215,7 @@ describe("FidoAuthenticatorService", () => { expect(userInterfaceSession.confirmNewCredential).toHaveBeenCalledWith({ credentialName: params.rpEntity.name, userName: params.userEntity.name, + userHandle: Fido2Utils.bufferToString(params.userEntity.id), userVerification, rpId: params.rpEntity.id, } as NewCredentialParams); diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts index 47d76897a3b..3464154b9cc 100644 --- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts +++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts @@ -112,6 +112,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr const response = await userInterfaceSession.confirmNewCredential({ credentialName: params.rpEntity.name, userName: params.userEntity.name, + userHandle: Fido2Utils.bufferToString(params.userEntity.id), userVerification: params.requireUserVerification, rpId: params.rpEntity.id, }); diff --git a/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts b/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts index 597f2d8f32e..aac447e0337 100644 --- a/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts +++ b/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts @@ -4,6 +4,7 @@ import { of } from "rxjs"; import { AuthService } from "../../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { DomainSettingsService } from "../../../autofill/services/domain-settings.service"; +import { Utils } from "../../../platform/misc/utils"; import { VaultSettingsService } from "../../../vault/abstractions/vault-settings/vault-settings.service"; import { ConfigService } from "../../abstractions/config/config.service"; import { @@ -17,7 +18,7 @@ import { CreateCredentialParams, FallbackRequestedError, } from "../../abstractions/fido2/fido2-client.service.abstraction"; -import { Utils } from "../../misc/utils"; +import { TaskSchedulerService } from "../../scheduling/task-scheduler.service"; import * as DomainUtils from "./domain-utils"; import { Fido2AuthenticatorService } from "./fido2-authenticator.service"; @@ -35,6 +36,7 @@ describe("FidoAuthenticatorService", () => { let authService!: MockProxy; let vaultSettingsService: MockProxy; let domainSettingsService: MockProxy; + let taskSchedulerService: MockProxy; let client!: Fido2ClientService; let tab!: chrome.tabs.Tab; let isValidRpId!: jest.SpyInstance; @@ -45,6 +47,7 @@ describe("FidoAuthenticatorService", () => { authService = mock(); vaultSettingsService = mock(); domainSettingsService = mock(); + taskSchedulerService = mock(); isValidRpId = jest.spyOn(DomainUtils, "isValidRpId"); @@ -54,6 +57,7 @@ describe("FidoAuthenticatorService", () => { authService, vaultSettingsService, domainSettingsService, + taskSchedulerService, ); configService.serverConfig$ = of({ environment: { vault: VaultUrl } } as any); vaultSettingsService.enablePasskeys$ = of(true); diff --git a/libs/common/src/platform/services/fido2/fido2-client.service.ts b/libs/common/src/platform/services/fido2/fido2-client.service.ts index d22b91fda05..b384fce1f12 100644 --- a/libs/common/src/platform/services/fido2/fido2-client.service.ts +++ b/libs/common/src/platform/services/fido2/fido2-client.service.ts @@ -1,4 +1,4 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, Subscription } from "rxjs"; import { parse } from "tldts"; import { AuthService } from "../../../auth/abstractions/auth.service"; @@ -27,6 +27,8 @@ import { } from "../../abstractions/fido2/fido2-client.service.abstraction"; import { LogService } from "../../abstractions/log.service"; import { Utils } from "../../misc/utils"; +import { ScheduledTaskNames } from "../../scheduling/scheduled-task-name.enum"; +import { TaskSchedulerService } from "../../scheduling/task-scheduler.service"; import { isValidRpId } from "./domain-utils"; import { Fido2Utils } from "./fido2-utils"; @@ -38,14 +40,33 @@ import { Fido2Utils } from "./fido2-utils"; * It is highly recommended that the W3C specification is used a reference when reading this code. */ export class Fido2ClientService implements Fido2ClientServiceAbstraction { + private timeoutAbortController: AbortController; + private readonly TIMEOUTS = { + NO_VERIFICATION: { + DEFAULT: 120000, + MIN: 30000, + MAX: 180000, + }, + WITH_VERIFICATION: { + DEFAULT: 300000, + MIN: 30000, + MAX: 600000, + }, + }; + constructor( private authenticator: Fido2AuthenticatorService, private configService: ConfigService, private authService: AuthService, private vaultSettingsService: VaultSettingsService, private domainSettingsService: DomainSettingsService, + private taskSchedulerService: TaskSchedulerService, private logService?: LogService, - ) {} + ) { + this.taskSchedulerService.registerTaskHandler(ScheduledTaskNames.fido2ClientAbortTimeout, () => + this.timeoutAbortController?.abort(), + ); + } async isFido2FeatureEnabled(hostname: string, origin: string): Promise { const isUserLoggedIn = @@ -161,7 +182,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { this.logService?.info(`[Fido2Client] Aborted with AbortController`); throw new DOMException("The operation either timed out or was not allowed.", "AbortError"); } - const timeout = setAbortTimeout( + const timeoutSubscription = this.setAbortTimeout( abortController, params.authenticatorSelection?.userVerification, params.timeout, @@ -210,7 +231,8 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { }; } - clearTimeout(timeout); + timeoutSubscription?.unsubscribe(); + return { credentialId: Fido2Utils.bufferToString(makeCredentialResult.credentialId), attestationObject: Fido2Utils.bufferToString(makeCredentialResult.attestationObject), @@ -273,7 +295,11 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { throw new DOMException("The operation either timed out or was not allowed.", "AbortError"); } - const timeout = setAbortTimeout(abortController, params.userVerification, params.timeout); + const timeoutSubscription = this.setAbortTimeout( + abortController, + params.userVerification, + params.timeout, + ); let getAssertionResult; try { @@ -310,7 +336,8 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { this.logService?.info(`[Fido2Client] Aborted with AbortController`); throw new DOMException("The operation either timed out or was not allowed.", "AbortError"); } - clearTimeout(timeout); + + timeoutSubscription?.unsubscribe(); return { authenticatorData: Fido2Utils.bufferToString(getAssertionResult.authenticatorData), @@ -323,43 +350,29 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { signature: Fido2Utils.bufferToString(getAssertionResult.signature), }; } -} -const TIMEOUTS = { - NO_VERIFICATION: { - DEFAULT: 120000, - MIN: 30000, - MAX: 180000, - }, - WITH_VERIFICATION: { - DEFAULT: 300000, - MIN: 30000, - MAX: 600000, - }, -}; + private setAbortTimeout = ( + abortController: AbortController, + userVerification?: UserVerification, + timeout?: number, + ): Subscription => { + let clampedTimeout: number; -function setAbortTimeout( - abortController: AbortController, - userVerification?: UserVerification, - timeout?: number, -): number { - let clampedTimeout: number; + const { WITH_VERIFICATION, NO_VERIFICATION } = this.TIMEOUTS; + if (userVerification === "required") { + timeout = timeout ?? WITH_VERIFICATION.DEFAULT; + clampedTimeout = Math.max(WITH_VERIFICATION.MIN, Math.min(timeout, WITH_VERIFICATION.MAX)); + } else { + timeout = timeout ?? NO_VERIFICATION.DEFAULT; + clampedTimeout = Math.max(NO_VERIFICATION.MIN, Math.min(timeout, NO_VERIFICATION.MAX)); + } - if (userVerification === "required") { - timeout = timeout ?? TIMEOUTS.WITH_VERIFICATION.DEFAULT; - clampedTimeout = Math.max( - TIMEOUTS.WITH_VERIFICATION.MIN, - Math.min(timeout, TIMEOUTS.WITH_VERIFICATION.MAX), + this.timeoutAbortController = abortController; + return this.taskSchedulerService.setTimeout( + ScheduledTaskNames.fido2ClientAbortTimeout, + clampedTimeout, ); - } else { - timeout = timeout ?? TIMEOUTS.NO_VERIFICATION.DEFAULT; - clampedTimeout = Math.max( - TIMEOUTS.NO_VERIFICATION.MIN, - Math.min(timeout, TIMEOUTS.NO_VERIFICATION.MAX), - ); - } - - return self.setTimeout(() => abortController.abort(), clampedTimeout); + }; } /** diff --git a/libs/common/src/platform/services/system.service.ts b/libs/common/src/platform/services/system.service.ts index a3927a3fb8f..382b3bf8e86 100644 --- a/libs/common/src/platform/services/system.service.ts +++ b/libs/common/src/platform/services/system.service.ts @@ -1,4 +1,4 @@ -import { firstValueFrom, map, timeout } from "rxjs"; +import { firstValueFrom, map, Subscription, timeout } from "rxjs"; import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; @@ -13,10 +13,12 @@ import { PlatformUtilsService } from "../abstractions/platform-utils.service"; import { SystemService as SystemServiceAbstraction } from "../abstractions/system.service"; import { BiometricStateService } from "../biometrics/biometric-state.service"; import { Utils } from "../misc/utils"; +import { ScheduledTaskNames } from "../scheduling/scheduled-task-name.enum"; +import { TaskSchedulerService } from "../scheduling/task-scheduler.service"; export class SystemService implements SystemServiceAbstraction { private reloadInterval: any = null; - private clearClipboardTimeout: any = null; + private clearClipboardTimeoutSubscription: Subscription; private clearClipboardTimeoutFunction: () => Promise = null; constructor( @@ -28,7 +30,13 @@ export class SystemService implements SystemServiceAbstraction { private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private biometricStateService: BiometricStateService, private accountService: AccountService, - ) {} + private taskSchedulerService: TaskSchedulerService, + ) { + this.taskSchedulerService.registerTaskHandler( + ScheduledTaskNames.systemClearClipboardTimeout, + () => this.clearPendingClipboard(), + ); + } async startProcessReload(authService: AuthService): Promise { const accounts = await firstValueFrom(this.accountService.accounts$); @@ -111,25 +119,22 @@ export class SystemService implements SystemServiceAbstraction { } async clearClipboard(clipboardValue: string, timeoutMs: number = null): Promise { - if (this.clearClipboardTimeout != null) { - clearTimeout(this.clearClipboardTimeout); - this.clearClipboardTimeout = null; - } + this.clearClipboardTimeoutSubscription?.unsubscribe(); if (Utils.isNullOrWhitespace(clipboardValue)) { return; } - const clearClipboardDelay = await firstValueFrom( - this.autofillSettingsService.clearClipboardDelay$, - ); - - if (clearClipboardDelay == null) { - return; + let taskTimeoutInMs = timeoutMs; + if (!taskTimeoutInMs) { + const clearClipboardDelayInSeconds = await firstValueFrom( + this.autofillSettingsService.clearClipboardDelay$, + ); + taskTimeoutInMs = clearClipboardDelayInSeconds ? clearClipboardDelayInSeconds * 1000 : null; } - if (timeoutMs == null) { - timeoutMs = clearClipboardDelay * 1000; + if (!taskTimeoutInMs) { + return; } this.clearClipboardTimeoutFunction = async () => { @@ -139,9 +144,10 @@ export class SystemService implements SystemServiceAbstraction { } }; - this.clearClipboardTimeout = setTimeout(async () => { - await this.clearPendingClipboard(); - }, timeoutMs); + this.clearClipboardTimeoutSubscription = this.taskSchedulerService.setTimeout( + ScheduledTaskNames.systemClearClipboardTimeout, + taskTimeoutInMs, + ); } async clearPendingClipboard() { diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 6cc2b181b64..0b55e7be77c 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -95,10 +95,6 @@ export const NEW_WEB_LAYOUT_BANNER_DISK = new StateDefinition("newWebLayoutBanne web: "disk-local", }); -export const UNASSIGNED_ITEMS_BANNER_DISK = new StateDefinition("unassignedItemsBanner", "disk", { - web: "disk-local", -}); - // Platform export const APPLICATION_ID_DISK = new StateDefinition("applicationId", "disk", { @@ -116,6 +112,7 @@ export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk"); export const ENVIRONMENT_MEMORY = new StateDefinition("environment", "memory"); export const THEMING_DISK = new StateDefinition("theming", "disk", { web: "disk-local" }); export const TRANSLATION_DISK = new StateDefinition("translation", "disk", { web: "disk-local" }); +export const TASK_SCHEDULER_DISK = new StateDefinition("taskScheduler", "disk"); // Secrets Manager diff --git a/libs/common/src/services/event/event-upload.service.ts b/libs/common/src/services/event/event-upload.service.ts index c87d3b2024d..faac95c4d6e 100644 --- a/libs/common/src/services/event/event-upload.service.ts +++ b/libs/common/src/services/event/event-upload.service.ts @@ -7,6 +7,8 @@ import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { EventData } from "../../models/data/event.data"; import { EventRequest } from "../../models/request/event.request"; import { LogService } from "../../platform/abstractions/log.service"; +import { ScheduledTaskNames } from "../../platform/scheduling/scheduled-task-name.enum"; +import { TaskSchedulerService } from "../../platform/scheduling/task-scheduler.service"; import { StateProvider } from "../../platform/state"; import { UserId } from "../../types/guid"; @@ -19,7 +21,12 @@ export class EventUploadService implements EventUploadServiceAbstraction { private stateProvider: StateProvider, private logService: LogService, private authService: AuthService, - ) {} + private taskSchedulerService: TaskSchedulerService, + ) { + this.taskSchedulerService.registerTaskHandler(ScheduledTaskNames.eventUploadsInterval, () => + this.uploadEvents(), + ); + } init(checkOnInterval: boolean) { if (this.inited) { @@ -28,10 +35,11 @@ export class EventUploadService implements EventUploadServiceAbstraction { this.inited = true; if (checkOnInterval) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.uploadEvents(); - setInterval(() => this.uploadEvents(), 60 * 1000); // check every 60 seconds + void this.uploadEvents(); + this.taskSchedulerService.setInterval( + ScheduledTaskNames.eventUploadsInterval, + 60 * 1000, // check every 60 seconds + ); } } diff --git a/libs/common/src/services/notifications.service.ts b/libs/common/src/services/notifications.service.ts index d5c7170e23c..8e6a664a0af 100644 --- a/libs/common/src/services/notifications.service.ts +++ b/libs/common/src/services/notifications.service.ts @@ -1,6 +1,6 @@ import * as signalR from "@microsoft/signalr"; import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, Subscription } from "rxjs"; import { LogoutReason } from "@bitwarden/auth/common"; @@ -20,6 +20,8 @@ import { EnvironmentService } from "../platform/abstractions/environment.service import { LogService } from "../platform/abstractions/log.service"; import { MessagingService } from "../platform/abstractions/messaging.service"; import { StateService } from "../platform/abstractions/state.service"; +import { ScheduledTaskNames } from "../platform/scheduling/scheduled-task-name.enum"; +import { TaskSchedulerService } from "../platform/scheduling/task-scheduler.service"; import { SyncService } from "../vault/abstractions/sync/sync.service.abstraction"; export class NotificationsService implements NotificationsServiceAbstraction { @@ -28,7 +30,8 @@ export class NotificationsService implements NotificationsServiceAbstraction { private connected = false; private inited = false; private inactive = false; - private reconnectTimer: any = null; + private reconnectTimerSubscription: Subscription; + private isSyncingOnReconnect = true; constructor( private logService: LogService, @@ -40,7 +43,12 @@ export class NotificationsService implements NotificationsServiceAbstraction { private stateService: StateService, private authService: AuthService, private messagingService: MessagingService, + private taskSchedulerService: TaskSchedulerService, ) { + this.taskSchedulerService.registerTaskHandler( + ScheduledTaskNames.notificationsReconnectTimeout, + () => this.reconnect(this.isSyncingOnReconnect), + ); this.environmentService.environment$.subscribe(() => { if (!this.inited) { return; @@ -213,10 +221,8 @@ export class NotificationsService implements NotificationsServiceAbstraction { } private async reconnect(sync: boolean) { - if (this.reconnectTimer != null) { - clearTimeout(this.reconnectTimer); - this.reconnectTimer = null; - } + this.reconnectTimerSubscription?.unsubscribe(); + if (this.connected || !this.inited || this.inactive) { return; } @@ -236,7 +242,11 @@ export class NotificationsService implements NotificationsServiceAbstraction { } if (!this.connected) { - this.reconnectTimer = setTimeout(() => this.reconnect(sync), this.random(120000, 300000)); + this.isSyncingOnReconnect = sync; + this.reconnectTimerSubscription = this.taskSchedulerService.setTimeout( + ScheduledTaskNames.notificationsReconnectTimeout, + this.random(120000, 300000), + ); } } diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts index 6a8071af0c0..487a2578b5e 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts @@ -2,6 +2,8 @@ import { MockProxy, any, mock } from "jest-mock-extended"; import { BehaviorSubject, from, of } from "rxjs"; import { LogoutReason } from "@bitwarden/auth/common"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { SearchService } from "../../abstractions/search.service"; @@ -37,6 +39,8 @@ describe("VaultTimeoutService", () => { let authService: MockProxy; let vaultTimeoutSettingsService: MockProxy; let stateEventRunnerService: MockProxy; + let taskSchedulerService: MockProxy; + let logService: MockProxy; let lockedCallback: jest.Mock, [userId: string]>; let loggedOutCallback: jest.Mock, [logoutReason: LogoutReason, userId?: string]>; @@ -60,6 +64,8 @@ describe("VaultTimeoutService", () => { authService = mock(); vaultTimeoutSettingsService = mock(); stateEventRunnerService = mock(); + taskSchedulerService = mock(); + logService = mock(); lockedCallback = jest.fn(); loggedOutCallback = jest.fn(); @@ -85,6 +91,8 @@ describe("VaultTimeoutService", () => { authService, vaultTimeoutSettingsService, stateEventRunnerService, + taskSchedulerService, + logService, lockedCallback, loggedOutCallback, ); diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.ts index 8d8ecd68a57..d9efef44f42 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -1,6 +1,8 @@ import { combineLatest, filter, firstValueFrom, map, switchMap, timeout } from "rxjs"; import { LogoutReason } from "@bitwarden/auth/common"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { SearchService } from "../../abstractions/search.service"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; @@ -35,12 +37,19 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { private authService: AuthService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private stateEventRunnerService: StateEventRunnerService, + private taskSchedulerService: TaskSchedulerService, + protected logService: LogService, private lockedCallback: (userId?: string) => Promise = null, private loggedOutCallback: ( logoutReason: LogoutReason, userId?: string, ) => Promise = null, - ) {} + ) { + this.taskSchedulerService.registerTaskHandler( + ScheduledTaskNames.vaultTimeoutCheckInterval, + () => this.checkVaultTimeout(), + ); + } async init(checkOnInterval: boolean) { if (this.inited) { @@ -54,10 +63,11 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { } startCheck() { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.checkVaultTimeout(); - setInterval(() => this.checkVaultTimeout(), 10 * 1000); // check every 10 seconds + this.checkVaultTimeout().catch((error) => this.logService.error(error)); + this.taskSchedulerService.setInterval( + ScheduledTaskNames.vaultTimeoutCheckInterval, + 10 * 1000, // check every 10 seconds + ); } async checkVaultTimeout(): Promise { diff --git a/libs/common/src/state-migrations/migrations/67-remove-unassigned-items-banner-dismissed.spec.ts b/libs/common/src/state-migrations/migrations/67-remove-unassigned-items-banner-dismissed.spec.ts new file mode 100644 index 00000000000..2404920a433 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/67-remove-unassigned-items-banner-dismissed.spec.ts @@ -0,0 +1,50 @@ +import { runMigrator } from "../migration-helper.spec"; +import { IRREVERSIBLE } from "../migrator"; + +import { RemoveUnassignedItemsBannerDismissed } from "./67-remove-unassigned-items-banner-dismissed"; + +describe("RemoveUnassignedItemsBannerDismissed", () => { + const sut = new RemoveUnassignedItemsBannerDismissed(66, 67); + + describe("migrate", () => { + it("deletes unassignedItemsBanner from all users", async () => { + const output = await runMigrator(sut, { + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + user2: { + email: "user2@email.com", + name: "User 2", + emailVerified: true, + }, + }, + user_user1_unassignedItemsBanner_showBanner: true, + user_user2_unassignedItemsBanner_showBanner: false, + }); + + expect(output).toEqual({ + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + user2: { + email: "user2@email.com", + name: "User 2", + emailVerified: true, + }, + }, + }); + }); + }); + + describe("rollback", () => { + it("is irreversible", async () => { + await expect(runMigrator(sut, {}, "rollback")).rejects.toThrow(IRREVERSIBLE); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/67-remove-unassigned-items-banner-dismissed.ts b/libs/common/src/state-migrations/migrations/67-remove-unassigned-items-banner-dismissed.ts new file mode 100644 index 00000000000..de3cee573df --- /dev/null +++ b/libs/common/src/state-migrations/migrations/67-remove-unassigned-items-banner-dismissed.ts @@ -0,0 +1,23 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { IRREVERSIBLE, Migrator } from "../migrator"; + +export const SHOW_BANNER: KeyDefinitionLike = { + key: "showBanner", + stateDefinition: { name: "unassignedItemsBanner" }, +}; + +export class RemoveUnassignedItemsBannerDismissed extends Migrator<66, 67> { + async migrate(helper: MigrationHelper): Promise { + await Promise.all( + (await helper.getAccounts()).map(async ({ userId }) => { + if (helper.getFromUser(userId, SHOW_BANNER) != null) { + await helper.removeFromUser(userId, SHOW_BANNER); + } + }), + ); + } + + async rollback(helper: MigrationHelper): Promise { + throw IRREVERSIBLE; + } +} diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index ba85f51c38b..e3019ab48d2 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -1,6 +1,8 @@ import { mock } from "jest-mock-extended"; import { BehaviorSubject, of } from "rxjs"; +import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; + import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeStateProvider } from "../../../spec/fake-state-provider"; import { makeStaticByteArray } from "../../../spec/utils"; @@ -114,6 +116,7 @@ describe("Cipher Service", () => { const i18nService = mock(); const searchService = mock(); const encryptService = mock(); + const bulkEncryptService = mock(); const configService = mock(); accountService = mockAccountServiceWith(mockUserId); const stateProvider = new FakeStateProvider(accountService); @@ -136,6 +139,7 @@ describe("Cipher Service", () => { stateService, autofillSettingsService, encryptService, + bulkEncryptService, cipherFileUploadService, configService, stateProvider, @@ -352,8 +356,10 @@ describe("Cipher Service", () => { const cipher1 = new CipherView(cipherObj); cipher1.id = "Cipher 1"; + cipher1.organizationId = null; const cipher2 = new CipherView(cipherObj); cipher2.id = "Cipher 2"; + cipher2.organizationId = null; decryptedCiphers = new BehaviorSubject({ Cipher1: cipher1, diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index d2d28c2d812..aa1d3a18279 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -1,6 +1,9 @@ import { firstValueFrom, map, Observable, skipWhile, switchMap } from "rxjs"; import { SemVer } from "semver"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; + import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service"; @@ -102,6 +105,7 @@ export class CipherService implements CipherServiceAbstraction { private stateService: StateService, private autofillSettingsService: AutofillSettingsServiceAbstraction, private encryptService: EncryptService, + private bulkEncryptService: BulkEncryptService, private cipherFileUploadService: CipherFileUploadService, private configService: ConfigService, private stateProvider: StateProvider, @@ -397,12 +401,19 @@ export class CipherService implements CipherServiceAbstraction { const decCiphers = ( await Promise.all( - Object.entries(grouped).map(([orgId, groupedCiphers]) => - this.encryptService.decryptItems( - groupedCiphers, - keys.orgKeys[orgId as OrganizationId] ?? keys.userKey, - ), - ), + Object.entries(grouped).map(async ([orgId, groupedCiphers]) => { + if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) { + return await this.bulkEncryptService.decryptItems( + groupedCiphers, + keys.orgKeys[orgId as OrganizationId] ?? keys.userKey, + ); + } else { + return await this.encryptService.decryptItems( + groupedCiphers, + keys.orgKeys[orgId as OrganizationId] ?? keys.userKey, + ); + } + }), ) ) .flat() @@ -515,7 +526,12 @@ export class CipherService implements CipherServiceAbstraction { const ciphers = response.data.map((cr) => new Cipher(new CipherData(cr))); const key = await this.cryptoService.getOrgKey(organizationId); - const decCiphers = await this.encryptService.decryptItems(ciphers, key); + let decCiphers: CipherView[] = []; + if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) { + decCiphers = await this.bulkEncryptService.decryptItems(ciphers, key); + } else { + decCiphers = await this.encryptService.decryptItems(ciphers, key); + } decCiphers.sort(this.getLocaleSortingFunction()); return decCiphers; @@ -1184,11 +1200,16 @@ export class CipherService implements CipherServiceAbstraction { let encryptedCiphers: CipherWithIdRequest[] = []; const ciphers = await this.getAllDecrypted(); - if (!ciphers || ciphers.length === 0) { + if (!ciphers) { + return encryptedCiphers; + } + + const userCiphers = ciphers.filter((c) => c.organizationId == null); + if (userCiphers.length === 0) { return encryptedCiphers; } encryptedCiphers = await Promise.all( - ciphers.map(async (cipher) => { + userCiphers.map(async (cipher) => { const encryptedCipher = await this.encrypt(cipher, newUserKey, originalUserKey); return new CipherWithIdRequest(encryptedCipher); }), diff --git a/libs/importer/src/components/import.component.html b/libs/importer/src/components/import.component.html index eda999ab47e..54df0ba4d27 100644 --- a/libs/importer/src/components/import.component.html +++ b/libs/importer/src/components/import.component.html @@ -382,6 +382,7 @@ formControlName="file" (change)="setSelectedFile($event)" hidden + class="tw-hidden" /> diff --git a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html index 0766435e1ce..f1f0363c999 100644 --- a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html +++ b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html @@ -10,5 +10,8 @@ {{ "sendTypeFile" | i18n }} + diff --git a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.ts b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.ts index 1463b448a6a..620dc77c995 100644 --- a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.ts +++ b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.ts @@ -1,23 +1,39 @@ import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { Router, RouterLink } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; -import { ButtonModule, MenuModule } from "@bitwarden/components"; +import { BadgeModule, ButtonModule, MenuModule } from "@bitwarden/components"; @Component({ selector: "tools-new-send-dropdown", templateUrl: "new-send-dropdown.component.html", standalone: true, - imports: [JslibModule, CommonModule, ButtonModule, RouterLink, MenuModule], + imports: [JslibModule, CommonModule, ButtonModule, RouterLink, MenuModule, BadgeModule], }) -export class NewSendDropdownComponent { +export class NewSendDropdownComponent implements OnInit { sendType = SendType; - constructor(private router: Router) {} + hasNoPremium = false; + + constructor( + private router: Router, + private billingAccountProfileStateService: BillingAccountProfileStateService, + ) {} + + async ngOnInit() { + this.hasNoPremium = !(await firstValueFrom( + this.billingAccountProfileStateService.hasPremiumFromAnySource$, + )); + } newItemNavigate(type: SendType) { + if (this.hasNoPremium && type === SendType.File) { + return this.router.navigate(["/premium"]); + } void this.router.navigate(["/add-send"], { queryParams: { type: type, isNew: true } }); } } diff --git a/libs/vault/src/cipher-form/cipher-form-container.ts b/libs/vault/src/cipher-form/cipher-form-container.ts index 0e4e34a6be6..9655b70bbbd 100644 --- a/libs/vault/src/cipher-form/cipher-form-container.ts +++ b/libs/vault/src/cipher-form/cipher-form-container.ts @@ -1,6 +1,9 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherFormConfig } from "@bitwarden/vault"; +import { AdditionalOptionsSectionComponent } from "./components/additional-options/additional-options-section.component"; import { CardDetailsSectionComponent } from "./components/card-details-section/card-details-section.component"; +import { CustomFieldsComponent } from "./components/custom-fields/custom-fields.component"; import { IdentitySectionComponent } from "./components/identity/identity.component"; import { ItemDetailsSectionComponent } from "./components/item-details/item-details-section.component"; @@ -10,19 +13,32 @@ import { ItemDetailsSectionComponent } from "./components/item-details/item-deta */ export type CipherForm = { itemDetails?: ItemDetailsSectionComponent["itemDetailsForm"]; + additionalOptions?: AdditionalOptionsSectionComponent["additionalOptionsForm"]; cardDetails?: CardDetailsSectionComponent["cardDetailsForm"]; identityDetails?: IdentitySectionComponent["identityForm"]; + customFields?: CustomFieldsComponent["customFieldsForm"]; }; /** * A container for the {@link CipherForm} that allows for registration of child form groups and patching of the cipher - * to be updated/created. Child form components inject this container in order to register themselves with the parent form. + * to be updated/created. Child form components inject this container in order to register themselves with the parent form + * and access configuration options. * * This is an alternative to passing the form groups down through the component tree via @Inputs() and form updates via * @Outputs(). It allows child forms to define their own structure and validation rules, while still being able to * update the parent cipher. */ export abstract class CipherFormContainer { + /** + * The configuration for the cipher form. + */ + readonly config: CipherFormConfig; + + /** + * The original cipher that is being edited/cloned. Used to pre-populate the form and compare changes. + */ + readonly originalCipherView: CipherView | null; + abstract registerChildForm( name: K, group: Exclude, diff --git a/libs/vault/src/cipher-form/cipher-form.stories.ts b/libs/vault/src/cipher-form/cipher-form.stories.ts index 47a1e90abcf..67011b5a478 100644 --- a/libs/vault/src/cipher-form/cipher-form.stories.ts +++ b/libs/vault/src/cipher-form/cipher-form.stories.ts @@ -7,6 +7,7 @@ import { moduleMetadata, StoryObj, } from "@storybook/angular"; +import { BehaviorSubject } from "rxjs"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -15,7 +16,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { AsyncActionsModule, ButtonModule, ToastService } from "@bitwarden/components"; -import { CipherFormConfig } from "@bitwarden/vault"; +import { CipherFormConfig, PasswordRepromptService } from "@bitwarden/vault"; import { PreloadedEnglishI18nModule } from "@bitwarden/web-vault/src/app/core/tests"; import { CipherFormService } from "./abstractions/cipher-form.service"; @@ -71,6 +72,7 @@ const defaultConfig: CipherFormConfig = { folderId: "folder2", collectionIds: ["col1"], favorite: false, + notes: "Example notes", } as unknown as Cipher, }; @@ -105,6 +107,12 @@ export default { showToast: action("showToast"), }, }, + { + provide: PasswordRepromptService, + useValue: { + enabled$: new BehaviorSubject(true), + }, + }, ], }), componentWrapperDecorator( diff --git a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.html b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.html new file mode 100644 index 00000000000..9f162cb25e8 --- /dev/null +++ b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.html @@ -0,0 +1,29 @@ + + +

{{ "additionalOptions" | i18n }}

+
+ + + + {{ "notes" | i18n }} + + + + + {{ "passwordPrompt" | i18n }} + + + + +
+ + diff --git a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts new file mode 100644 index 00000000000..d488fc9db91 --- /dev/null +++ b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts @@ -0,0 +1,117 @@ +import { Component } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +import { PasswordRepromptService } from "../../../services/password-reprompt.service"; +import { CipherFormContainer } from "../../cipher-form-container"; +import { CustomFieldsComponent } from "../custom-fields/custom-fields.component"; + +import { AdditionalOptionsSectionComponent } from "./additional-options-section.component"; + +@Component({ + standalone: true, + selector: "vault-custom-fields", + template: "", +}) +class MockCustomFieldsComponent {} + +describe("AdditionalOptionsSectionComponent", () => { + let component: AdditionalOptionsSectionComponent; + let fixture: ComponentFixture; + let cipherFormProvider: MockProxy; + let passwordRepromptService: MockProxy; + let passwordRepromptEnabled$: BehaviorSubject; + + beforeEach(async () => { + cipherFormProvider = mock(); + + passwordRepromptService = mock(); + passwordRepromptEnabled$ = new BehaviorSubject(true); + passwordRepromptService.enabled$ = passwordRepromptEnabled$; + + await TestBed.configureTestingModule({ + imports: [AdditionalOptionsSectionComponent], + providers: [ + { provide: CipherFormContainer, useValue: cipherFormProvider }, + { provide: PasswordRepromptService, useValue: passwordRepromptService }, + { provide: I18nService, useValue: mock() }, + ], + }) + .overrideComponent(AdditionalOptionsSectionComponent, { + remove: { + imports: [CustomFieldsComponent], + }, + add: { + imports: [MockCustomFieldsComponent], + }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(AdditionalOptionsSectionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("registers 'additionalOptionsForm' form with CipherFormContainer", () => { + expect(cipherFormProvider.registerChildForm).toHaveBeenCalledWith( + "additionalOptions", + component.additionalOptionsForm, + ); + }); + + it("patches 'additionalOptionsForm' changes to CipherFormContainer", () => { + component.additionalOptionsForm.patchValue({ + notes: "new notes", + reprompt: true, + }); + + expect(cipherFormProvider.patchCipher).toHaveBeenCalledWith({ + notes: "new notes", + reprompt: 1, + }); + }); + + it("disables 'additionalOptionsForm' when in partial-edit mode", () => { + cipherFormProvider.config.mode = "partial-edit"; + + component.ngOnInit(); + + expect(component.additionalOptionsForm.disabled).toBe(true); + }); + + it("initializes 'additionalOptionsForm' with original cipher view values", () => { + (cipherFormProvider.originalCipherView as any) = { + notes: "original notes", + reprompt: 1, + } as CipherView; + + component.ngOnInit(); + + expect(component.additionalOptionsForm.value).toEqual({ + notes: "original notes", + reprompt: true, + }); + }); + + it("hides password reprompt checkbox when disabled", () => { + passwordRepromptEnabled$.next(true); + fixture.detectChanges(); + + let checkbox = fixture.nativeElement.querySelector("input[formControlName='reprompt']"); + expect(checkbox).not.toBeNull(); + + passwordRepromptEnabled$.next(false); + fixture.detectChanges(); + + checkbox = fixture.nativeElement.querySelector("input[formControlName='reprompt']"); + expect(checkbox).toBeNull(); + }); +}); diff --git a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts new file mode 100644 index 00000000000..6c061e1eeab --- /dev/null +++ b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts @@ -0,0 +1,105 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectorRef, Component, OnInit, ViewChild } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; +import { shareReplay } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums"; +import { + CardComponent, + CheckboxModule, + FormFieldModule, + LinkModule, + SectionComponent, + SectionHeaderComponent, + TypographyModule, +} from "@bitwarden/components"; + +import { PasswordRepromptService } from "../../../services/password-reprompt.service"; +import { CipherFormContainer } from "../../cipher-form-container"; +import { CustomFieldsComponent } from "../custom-fields/custom-fields.component"; + +@Component({ + selector: "vault-additional-options-section", + templateUrl: "./additional-options-section.component.html", + standalone: true, + imports: [ + CommonModule, + SectionComponent, + SectionHeaderComponent, + TypographyModule, + JslibModule, + CardComponent, + FormFieldModule, + ReactiveFormsModule, + CheckboxModule, + CommonModule, + CustomFieldsComponent, + LinkModule, + ], +}) +export class AdditionalOptionsSectionComponent implements OnInit { + @ViewChild(CustomFieldsComponent) customFieldsComponent: CustomFieldsComponent; + + additionalOptionsForm = this.formBuilder.group({ + notes: [null as string], + reprompt: [false], + }); + + passwordRepromptEnabled$ = this.passwordRepromptService.enabled$.pipe( + shareReplay({ refCount: false, bufferSize: 1 }), + ); + + /** When false when the add field button should be displayed in the Additional Options section */ + hasCustomFields = false; + + /** True when the form is in `partial-edit` mode */ + isPartialEdit = false; + + constructor( + private cipherFormContainer: CipherFormContainer, + private formBuilder: FormBuilder, + private passwordRepromptService: PasswordRepromptService, + private changeDetectorRef: ChangeDetectorRef, + ) { + this.cipherFormContainer.registerChildForm("additionalOptions", this.additionalOptionsForm); + + this.additionalOptionsForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => { + this.cipherFormContainer.patchCipher({ + notes: value.notes, + reprompt: value.reprompt ? CipherRepromptType.Password : CipherRepromptType.None, + }); + }); + } + + ngOnInit() { + if (this.cipherFormContainer.originalCipherView) { + this.additionalOptionsForm.patchValue({ + notes: this.cipherFormContainer.originalCipherView.notes, + reprompt: + this.cipherFormContainer.originalCipherView.reprompt === CipherRepromptType.Password, + }); + } + + if (this.cipherFormContainer.config.mode === "partial-edit") { + this.additionalOptionsForm.disable(); + this.isPartialEdit = true; + } + } + + /** Opens the add custom field dialog */ + addCustomField() { + this.customFieldsComponent.openAddEditCustomFieldDialog(); + } + + /** Update the local state when the number of fields changes */ + handleCustomFieldChange(numberOfCustomFields: number) { + this.hasCustomFields = numberOfCustomFields > 0; + + // The event that triggers `handleCustomFieldChange` can occur within + // the CustomFieldComponent `ngOnInit` lifecycle hook, so we need to + // manually trigger change detection to update the view. + this.changeDetectorRef.detectChanges(); + } +} diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.html b/libs/vault/src/cipher-form/components/cipher-form.component.html index 8b5d4708997..669f3c8b963 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.html +++ b/libs/vault/src/cipher-form/components/cipher-form.component.html @@ -18,6 +18,8 @@ [disabled]="config.mode === 'partial-edit'" > + + diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.ts b/libs/vault/src/cipher-form/components/cipher-form.component.ts index 3307425e662..6f01f65be85 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-form.component.ts @@ -16,7 +16,7 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { AsyncActionsModule, @@ -35,6 +35,7 @@ import { CipherFormConfig } from "../abstractions/cipher-form-config.service"; import { CipherFormService } from "../abstractions/cipher-form.service"; import { CipherForm, CipherFormContainer } from "../cipher-form-container"; +import { AdditionalOptionsSectionComponent } from "./additional-options/additional-options-section.component"; import { CardDetailsSectionComponent } from "./card-details-section/card-details-section.component"; import { IdentitySectionComponent } from "./identity/identity.component"; import { ItemDetailsSectionComponent } from "./item-details/item-details-section.component"; @@ -62,6 +63,7 @@ import { ItemDetailsSectionComponent } from "./item-details/item-details-section CardDetailsSectionComponent, IdentitySectionComponent, NgIf, + AdditionalOptionsSectionComponent, ], }) export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, CipherFormContainer { @@ -91,24 +93,24 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci */ @Output() cipherSaved = new EventEmitter(); + /** + * The original cipher being edited or cloned. Null for add mode. + */ + originalCipherView: CipherView | null; + /** * The form group for the cipher. Starts empty and is populated by child components via the `registerChildForm` method. * @protected */ protected cipherForm = this.formBuilder.group({}); - /** - * The original cipher being edited or cloned. Null for add mode. - * @protected - */ - protected originalCipherView: CipherView | null; - /** * The value of the updated cipher. Starts as a new cipher (or clone of originalCipher) and is updated * by child components via the `patchCipher` method. * @protected */ protected updatedCipherView: CipherView | null; + protected loading: boolean = true; CipherType = CipherType; @@ -184,6 +186,10 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci this.updatedCipherView = Object.assign(this.updatedCipherView, this.originalCipherView); } else { this.updatedCipherView.type = this.config.cipherType; + + if (this.config.cipherType === CipherType.SecureNote) { + this.updatedCipherView.secureNote.type = SecureNoteType.Generic; + } } this.loading = false; diff --git a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.html b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.html new file mode 100644 index 00000000000..5f33d10b7d5 --- /dev/null +++ b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.html @@ -0,0 +1,48 @@ + + + + {{ (variant === "add" ? "addField" : "editField") | i18n }} + +
+ + {{ "fieldType" | i18n }} + + + + + {{ getTypeHint() }} + + + + + {{ "fieldLabel" | i18n }} + + + {{ "linkedLabelHelpText" | i18n }} + + +
+
+ + + + +
+
+ diff --git a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts new file mode 100644 index 00000000000..3ecb04cdc5c --- /dev/null +++ b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts @@ -0,0 +1,72 @@ +import { DIALOG_DATA } from "@angular/cdk/dialog"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { FieldType } from "@bitwarden/common/vault/enums"; + +import { + AddEditCustomFieldDialogComponent, + AddEditCustomFieldDialogData, +} from "./add-edit-custom-field-dialog.component"; + +describe("AddEditCustomFieldDialogComponent", () => { + let component: AddEditCustomFieldDialogComponent; + let fixture: ComponentFixture; + const addField = jest.fn(); + const updateLabel = jest.fn(); + const removeField = jest.fn(); + + const dialogData = { + addField, + updateLabel, + removeField, + } as AddEditCustomFieldDialogData; + + beforeEach(async () => { + addField.mockClear(); + updateLabel.mockClear(); + removeField.mockClear(); + + await TestBed.configureTestingModule({ + imports: [AddEditCustomFieldDialogComponent], + providers: [ + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: DIALOG_DATA, useValue: dialogData }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(AddEditCustomFieldDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges; + }); + + it("creates", () => { + expect(component).toBeTruthy(); + }); + + it("calls `addField` from DIALOG_DATA on with the type and label", () => { + component.customFieldForm.setValue({ type: FieldType.Text, label: "Test Label" }); + + component.submit(); + + expect(addField).toHaveBeenCalledWith(FieldType.Text, "Test Label"); + }); + + it("calls `updateLabel` from DIALOG_DATA with the new label", () => { + component.variant = "edit"; + dialogData.editLabelConfig = { index: 0, label: "Test Label" }; + component.customFieldForm.setValue({ type: FieldType.Text, label: "Test Label 2" }); + + component.submit(); + + expect(updateLabel).toHaveBeenCalledWith(0, "Test Label 2"); + }); + + it("calls `removeField` from DIALOG_DATA with the respective index", () => { + dialogData.editLabelConfig = { index: 2, label: "Test Label" }; + + component.removeField(); + + expect(removeField).toHaveBeenCalledWith(2); + }); +}); diff --git a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts new file mode 100644 index 00000000000..f08d0ca40ed --- /dev/null +++ b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts @@ -0,0 +1,120 @@ +import { DIALOG_DATA } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, Inject } from "@angular/core"; +import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { FieldType } from "@bitwarden/common/vault/enums"; +import { + AsyncActionsModule, + ButtonModule, + DialogModule, + FormFieldModule, + IconButtonModule, + SelectModule, +} from "@bitwarden/components"; + +export type AddEditCustomFieldDialogData = { + addField: (type: FieldType, label: string) => void; + updateLabel: (index: number, label: string) => void; + removeField: (index: number) => void; + /** When provided, dialog will display edit label variants */ + editLabelConfig?: { index: number; label: string }; +}; + +@Component({ + standalone: true, + selector: "vault-add-edit-custom-field-dialog", + templateUrl: "./add-edit-custom-field-dialog.component.html", + imports: [ + CommonModule, + JslibModule, + DialogModule, + ButtonModule, + FormFieldModule, + SelectModule, + ReactiveFormsModule, + IconButtonModule, + AsyncActionsModule, + ], +}) +export class AddEditCustomFieldDialogComponent { + variant: "add" | "edit"; + + customFieldForm = this.formBuilder.group({ + type: FieldType.Text, + label: ["", Validators.required], + }); + + fieldTypeOptions = [ + { name: this.i18nService.t("cfTypeText"), value: FieldType.Text }, + { name: this.i18nService.t("cfTypeHidden"), value: FieldType.Hidden }, + { name: this.i18nService.t("cfTypeBoolean"), value: FieldType.Boolean }, + { name: this.i18nService.t("cfTypeLinked"), value: FieldType.Linked }, + ]; + + FieldType = FieldType; + + constructor( + @Inject(DIALOG_DATA) private data: AddEditCustomFieldDialogData, + private formBuilder: FormBuilder, + private i18nService: I18nService, + ) { + this.variant = data.editLabelConfig ? "edit" : "add"; + + if (this.variant === "edit") { + this.customFieldForm.controls.label.setValue(data.editLabelConfig.label); + this.customFieldForm.controls.type.disable(); + } + } + + getTypeHint(): string { + switch (this.customFieldForm.get("type")?.value) { + case FieldType.Text: + return this.i18nService.t("textHelpText"); + case FieldType.Hidden: + return this.i18nService.t("hiddenHelpText"); + case FieldType.Boolean: + return this.i18nService.t("checkBoxHelpText"); + case FieldType.Linked: + return this.i18nService.t("linkedHelpText"); + default: + return ""; + } + } + + /** Direct the form submission to the proper action */ + submit = () => { + if (this.variant === "add") { + this.addField(); + } else { + this.updateLabel(); + } + }; + + /** Invoke the `addField` callback with the custom field details */ + addField() { + if (this.customFieldForm.invalid) { + return; + } + + const { type, label } = this.customFieldForm.value; + this.data.addField(type, label); + } + + /** Invoke the `updateLabel` callback with the new label */ + updateLabel() { + if (this.customFieldForm.invalid) { + return; + } + + const { label } = this.customFieldForm.value; + this.data.updateLabel(this.data.editLabelConfig.index, label); + } + + /** Invoke the `removeField` callback */ + removeField() { + this.data.removeField(this.data.editLabelConfig.index); + } +} diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html new file mode 100644 index 00000000000..49362b9421f --- /dev/null +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html @@ -0,0 +1,111 @@ + + +

{{ "customFields" | i18n }}

+
+
+ +
+ + + {{ field.value.name }} + + + + + + {{ field.value.name }} + + + + + + + + {{ field.value.name }} + + + + + {{ field.value.name }} + + + + + + + + +
+ + +
+
+
diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts new file mode 100644 index 00000000000..7befcd59b0a --- /dev/null +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts @@ -0,0 +1,373 @@ +import { LiveAnnouncer } from "@angular/cdk/a11y"; +import { DialogRef } from "@angular/cdk/dialog"; +import { CdkDragDrop } from "@angular/cdk/drag-drop"; +import { DebugElement } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherType, FieldType, LoginLinkedId } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; +import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; +import { DialogService } from "@bitwarden/components"; + +import { BitPasswordInputToggleDirective } from "../../../../../components/src/form-field/password-input-toggle.directive"; +import { CipherFormContainer } from "../../cipher-form-container"; + +import { CustomField, CustomFieldsComponent } from "./custom-fields.component"; + +const mockFieldViews = [ + { type: FieldType.Text, name: "text label", value: "text value" }, + { type: FieldType.Hidden, name: "hidden label", value: "hidden value" }, + { type: FieldType.Boolean, name: "boolean label", value: "true" }, + { type: FieldType.Linked, name: "linked label", value: null, linkedId: 1 }, +] as FieldView[]; + +let originalCipherView: CipherView | null = new CipherView(); +originalCipherView.type = CipherType.Login; +originalCipherView.login = new LoginView(); + +describe("CustomFieldsComponent", () => { + let component: CustomFieldsComponent; + let fixture: ComponentFixture; + let open: jest.Mock; + let announce: jest.Mock; + let patchCipher: jest.Mock; + + beforeEach(async () => { + open = jest.fn(); + announce = jest.fn().mockResolvedValue(null); + patchCipher = jest.fn(); + originalCipherView = new CipherView(); + originalCipherView.type = CipherType.Login; + originalCipherView.login = new LoginView(); + + await TestBed.configureTestingModule({ + imports: [CustomFieldsComponent], + providers: [ + { + provide: I18nService, + useValue: { t: (...keys: string[]) => keys.filter(Boolean).join(" ") }, + }, + { + provide: CipherFormContainer, + useValue: { patchCipher, originalCipherView, registerChildForm: jest.fn(), config: {} }, + }, + { + provide: LiveAnnouncer, + useValue: { announce }, + }, + ], + }) + .overrideProvider(DialogService, { + useValue: { + open, + }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(CustomFieldsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe("initializing", () => { + it("populates linkedFieldOptions", () => { + originalCipherView.login.linkedFieldOptions = new Map([ + [1, { i18nKey: "one-i18", propertyKey: "one" }], + [2, { i18nKey: "two-i18", propertyKey: "two" }], + ]); + + component.ngOnInit(); + + expect(component.linkedFieldOptions).toEqual([ + { value: 1, name: "one-i18" }, + { value: 2, name: "two-i18" }, + ]); + }); + + it("populates customFieldsForm", () => { + originalCipherView.fields = mockFieldViews; + + component.ngOnInit(); + + expect(component.fields.value).toEqual([ + { + linkedId: null, + name: "text label", + type: FieldType.Text, + value: "text value", + newField: false, + }, + { + linkedId: null, + name: "hidden label", + type: FieldType.Hidden, + value: "hidden value", + newField: false, + }, + { + linkedId: null, + name: "boolean label", + type: FieldType.Boolean, + value: true, + newField: false, + }, + { linkedId: 1, name: "linked label", type: FieldType.Linked, value: null, newField: false }, + ]); + }); + + it("forbids a user to view hidden fields when the cipher `viewPassword` is false", () => { + originalCipherView.viewPassword = false; + originalCipherView.fields = mockFieldViews; + + component.ngOnInit(); + + fixture.detectChanges(); + + const button = fixture.debugElement.query(By.directive(BitPasswordInputToggleDirective)); + + expect(button.nativeElement.disabled).toBe(true); + }); + }); + + describe("adding new field", () => { + let close: jest.Mock; + + beforeEach(() => { + close = jest.fn(); + component.dialogRef = { close } as unknown as DialogRef; + }); + + it("closes the add dialog", () => { + component.addField(FieldType.Text, "test label"); + + expect(close).toHaveBeenCalled(); + }); + + it("adds a unselected boolean field", () => { + component.addField(FieldType.Boolean, "bool label"); + + expect(component.fields.value).toEqual([ + { + linkedId: null, + name: "bool label", + type: FieldType.Boolean, + value: false, + newField: true, + }, + ]); + }); + + it("auto-selects the first linked field option", () => { + component.linkedFieldOptions = [ + { value: LoginLinkedId.Password, name: "one" }, + { value: LoginLinkedId.Username, name: "two" }, + ]; + + component.addField(FieldType.Linked, "linked label"); + + expect(component.fields.value).toEqual([ + { + linkedId: LoginLinkedId.Password, + name: "linked label", + type: FieldType.Linked, + value: null, + newField: true, + }, + ]); + }); + + it("adds text field", () => { + component.addField(FieldType.Text, "text label"); + + expect(component.fields.value).toEqual([ + { linkedId: null, name: "text label", type: FieldType.Text, value: null, newField: true }, + ]); + }); + + it("adds hidden field", () => { + component.addField(FieldType.Hidden, "hidden label"); + + expect(component.fields.value).toEqual([ + { + linkedId: null, + name: "hidden label", + type: FieldType.Hidden, + value: null, + newField: true, + }, + ]); + }); + + it("announces the new input field", () => { + component.addField(FieldType.Text, "text label 2"); + + fixture.detectChanges(); + + expect(announce).toHaveBeenCalledWith("fieldAdded text label 2", "polite"); + }); + + it("allows a user to view hidden fields when the cipher `viewPassword` is false", () => { + originalCipherView.viewPassword = false; + component.addField(FieldType.Hidden, "Hidden label"); + + fixture.detectChanges(); + + const button = fixture.debugElement.query(By.directive(BitPasswordInputToggleDirective)); + + expect(button.nativeElement.disabled).toBe(false); + }); + }); + + describe("updating a field", () => { + beforeEach(() => { + originalCipherView.fields = [mockFieldViews[0]]; + + component.ngOnInit(); + }); + + it("updates the value", () => { + component.fields.at(0).patchValue({ value: "new text value" }); + + const fieldView = new FieldView(); + fieldView.name = "text label"; + fieldView.value = "new text value"; + fieldView.type = FieldType.Text; + + expect(patchCipher).toHaveBeenCalledWith({ fields: [fieldView] }); + }); + + it("updates the label", () => { + component.updateLabel(0, "new text label"); + + const fieldView = new FieldView(); + fieldView.name = "new text label"; + fieldView.value = "text value"; + fieldView.type = FieldType.Text; + + expect(patchCipher).toHaveBeenCalledWith({ fields: [fieldView] }); + }); + }); + + describe("removing field", () => { + beforeEach(() => { + originalCipherView.fields = [mockFieldViews[0]]; + + component.ngOnInit(); + }); + + it("removes the field", () => { + component.removeField(0); + + expect(patchCipher).toHaveBeenCalledWith({ fields: [] }); + }); + }); + + describe("reordering fields", () => { + let toggleItems: DebugElement[]; + + beforeEach(() => { + originalCipherView.fields = mockFieldViews; + + component.ngOnInit(); + + fixture.detectChanges(); + + toggleItems = fixture.debugElement.queryAll( + By.css('button[data-testid="reorder-toggle-button"]'), + ); + }); + + it("reorders the fields when dropped", () => { + expect(component.fields.value.map((f: CustomField) => f.name)).toEqual([ + "text label", + "hidden label", + "boolean label", + "linked label", + ]); + + // Move second field to first + component.drop({ previousIndex: 0, currentIndex: 1 } as CdkDragDrop); + + const latestCallParams = patchCipher.mock.lastCall[0]; + + expect(latestCallParams.fields.map((f: FieldView) => f.name)).toEqual([ + "hidden label", + "text label", + "boolean label", + "linked label", + ]); + }); + + it("moves an item down in order via keyboard", () => { + // Move 3rd item (boolean label) down to 4th + toggleItems[2].triggerEventHandler("keydown", { + key: "ArrowDown", + preventDefault: jest.fn(), + }); + + const latestCallParams = patchCipher.mock.lastCall[0]; + + expect(latestCallParams.fields.map((f: FieldView) => f.name)).toEqual([ + "text label", + "hidden label", + "linked label", + "boolean label", + ]); + }); + + it("moves an item up in order via keyboard", () => { + // Move 2nd item (hidden label) up to 1st + toggleItems[1].triggerEventHandler("keydown", { key: "ArrowUp", preventDefault: jest.fn() }); + + const latestCallParams = patchCipher.mock.lastCall[0]; + + expect(latestCallParams.fields.map((f: FieldView) => f.name)).toEqual([ + "hidden label", + "text label", + "boolean label", + "linked label", + ]); + }); + + it("does not move the first item up", () => { + patchCipher.mockClear(); + + toggleItems[0].triggerEventHandler("keydown", { key: "ArrowUp", preventDefault: jest.fn() }); + + expect(patchCipher).not.toHaveBeenCalled(); + }); + + it("does not move the last item down", () => { + patchCipher.mockClear(); + + toggleItems[toggleItems.length - 1].triggerEventHandler("keydown", { + key: "ArrowDown", + preventDefault: jest.fn(), + }); + + expect(patchCipher).not.toHaveBeenCalled(); + }); + + it("announces the reorder up", () => { + // Move 2nd item up to 1st + toggleItems[1].triggerEventHandler("keydown", { key: "ArrowUp", preventDefault: jest.fn() }); + + // "reorder hidden label to position 1 of 4" + expect(announce).toHaveBeenCalledWith("reorderFieldUp hidden label 1 4", "assertive"); + }); + + it("announces the reorder down", () => { + // Move 3rd item down to 4th + toggleItems[2].triggerEventHandler("keydown", { + key: "ArrowDown", + preventDefault: jest.fn(), + }); + + // "reorder boolean label to position 4 of 4" + expect(announce).toHaveBeenCalledWith("reorderFieldDown boolean label 4 4", "assertive"); + }); + }); +}); diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts new file mode 100644 index 00000000000..0233e1c1b17 --- /dev/null +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts @@ -0,0 +1,334 @@ +import { LiveAnnouncer } from "@angular/cdk/a11y"; +import { DialogRef } from "@angular/cdk/dialog"; +import { CdkDragDrop, DragDropModule, moveItemInArray } from "@angular/cdk/drag-drop"; +import { CommonModule } from "@angular/common"; +import { + AfterViewInit, + Component, + DestroyRef, + ElementRef, + EventEmitter, + OnInit, + Output, + QueryList, + ViewChildren, + inject, +} from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormArray, FormBuilder, FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { Subject, switchMap, take } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FieldType, LinkedIdType } from "@bitwarden/common/vault/enums"; +import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; +import { + DialogService, + SectionComponent, + SectionHeaderComponent, + FormFieldModule, + TypographyModule, + CardComponent, + IconButtonModule, + CheckboxModule, + SelectModule, + LinkModule, +} from "@bitwarden/components"; + +import { CipherFormContainer } from "../../cipher-form-container"; + +import { + AddEditCustomFieldDialogComponent, + AddEditCustomFieldDialogData, +} from "./add-edit-custom-field-dialog/add-edit-custom-field-dialog.component"; + +/** Attributes associated with each individual FormGroup within the FormArray */ +export type CustomField = { + type: FieldType; + name: string; + value: string | boolean | null; + linkedId: LinkedIdType; + /** + * `newField` is set to true when the custom field is created. + * + * This is applicable when the user is adding a new field but + * the `viewPassword` property on the cipher is false. The + * user will still need the ability to set the value of the field + * they just created. + * + * See {@link CustomFieldsComponent.canViewPasswords} for implementation. + */ + newField: boolean; +}; + +@Component({ + standalone: true, + selector: "vault-custom-fields", + templateUrl: "./custom-fields.component.html", + imports: [ + JslibModule, + CommonModule, + FormsModule, + FormFieldModule, + ReactiveFormsModule, + SectionComponent, + SectionHeaderComponent, + TypographyModule, + CardComponent, + IconButtonModule, + CheckboxModule, + SelectModule, + DragDropModule, + LinkModule, + ], +}) +export class CustomFieldsComponent implements OnInit, AfterViewInit { + @Output() numberOfFieldsChange = new EventEmitter(); + + @ViewChildren("customFieldRow") customFieldRows: QueryList>; + + customFieldsForm = this.formBuilder.group({ + fields: new FormArray([]), + }); + + /** Reference to the add field dialog */ + dialogRef: DialogRef; + + /** Options for Linked Fields */ + linkedFieldOptions: { name: string; value: LinkedIdType }[] = []; + + /** True when edit/reorder toggles should be hidden based on partial-edit */ + isPartialEdit: boolean; + + /** True when there are custom fields available */ + hasCustomFields = false; + + /** Emits when a new custom field should be focused */ + private focusOnNewInput$ = new Subject(); + + destroyed$: DestroyRef; + FieldType = FieldType; + + constructor( + private dialogService: DialogService, + private cipherFormContainer: CipherFormContainer, + private formBuilder: FormBuilder, + private i18nService: I18nService, + private liveAnnouncer: LiveAnnouncer, + ) { + this.destroyed$ = inject(DestroyRef); + this.cipherFormContainer.registerChildForm("customFields", this.customFieldsForm); + + this.customFieldsForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((values) => { + this.updateCipher(values.fields); + }); + } + + /** Fields form array, referenced via a getter to avoid type-casting in multiple places */ + get fields(): FormArray { + return this.customFieldsForm.controls.fields as FormArray; + } + + ngOnInit() { + // Populate options for linked custom fields + this.linkedFieldOptions = Array.from( + this.cipherFormContainer.originalCipherView?.linkedFieldOptions?.entries() ?? [], + ) + .map(([id, linkedFieldOption]) => ({ + name: this.i18nService.t(linkedFieldOption.i18nKey), + value: id, + })) + .sort(Utils.getSortFunction(this.i18nService, "name")); + + // Populate the form with the existing fields + this.cipherFormContainer.originalCipherView?.fields?.forEach((field) => { + let value: string | boolean = field.value; + + if (field.type === FieldType.Boolean) { + value = field.value === "true" ? true : false; + } + + this.fields.push( + this.formBuilder.group({ + type: field.type, + name: field.name, + value: value, + linkedId: field.linkedId, + newField: false, + }), + ); + }); + + // Disable the form if in partial-edit mode + // Must happen after the initial fields are populated + if (this.cipherFormContainer.config.mode === "partial-edit") { + this.isPartialEdit = true; + this.customFieldsForm.disable(); + } + } + + ngAfterViewInit(): void { + // Focus on the new input field when it is added + // This is done after the view is initialized to ensure the input is rendered + this.focusOnNewInput$ + .pipe( + takeUntilDestroyed(this.destroyed$), + // QueryList changes are emitted after the view is updated + switchMap(() => this.customFieldRows.changes.pipe(take(1))), + ) + .subscribe(() => { + const input = + this.customFieldRows.last.nativeElement.querySelector("input"); + const label = document.querySelector(`label[for="${input.id}"]`).textContent.trim(); + + // Focus the input after the announcement element is added to the DOM, + // this should stop the announcement from being cut off by the "focus" event. + void this.liveAnnouncer + .announce(this.i18nService.t("fieldAdded", label), "polite") + .then(() => { + input.focus(); + }); + }); + } + + /** Opens the add/edit custom field dialog */ + openAddEditCustomFieldDialog(editLabelConfig?: AddEditCustomFieldDialogData["editLabelConfig"]) { + this.dialogRef = this.dialogService.open( + AddEditCustomFieldDialogComponent, + { + data: { + addField: this.addField.bind(this), + updateLabel: this.updateLabel.bind(this), + removeField: this.removeField.bind(this), + editLabelConfig, + }, + }, + ); + } + + /** Returns true when the user has permission to view passwords for the individual cipher */ + canViewPasswords(index: number) { + if (this.cipherFormContainer.originalCipherView === null) { + return true; + } + + return ( + this.cipherFormContainer.originalCipherView.viewPassword || + this.fields.at(index).value.newField + ); + } + + /** Updates label for an individual field */ + updateLabel(index: number, label: string) { + this.fields.at(index).patchValue({ name: label }); + this.dialogRef?.close(); + } + + /** Removes an individual field at a specific index */ + removeField(index: number) { + this.fields.removeAt(index); + this.dialogRef?.close(); + } + + /** Adds a new field to the form */ + addField(type: FieldType, label: string) { + this.dialogRef?.close(); + + let value = null; + let linkedId = null; + + if (type === FieldType.Boolean) { + // Default to false for boolean fields + value = false; + } + + if (type === FieldType.Linked && this.linkedFieldOptions.length > 0) { + // Default to the first linked field option + linkedId = this.linkedFieldOptions[0].value; + } + + this.fields.push( + this.formBuilder.group({ + type, + name: label, + value, + linkedId, + newField: true, + }), + ); + + // Trigger focus on the new input field + this.focusOnNewInput$.next(); + } + + /** Reorder the controls to match the new order after a "drop" event */ + drop(event: CdkDragDrop) { + // Alter the order of the fields array in place + moveItemInArray(this.fields.controls, event.previousIndex, event.currentIndex); + + this.updateCipher(this.fields.controls.map((control) => control.value)); + } + + /** Move a custom field up or down in the list order */ + async handleKeyDown(event: KeyboardEvent, label: string, index: number) { + if (event.key === "ArrowUp" && index !== 0) { + event.preventDefault(); + + const currentIndex = index - 1; + this.drop({ previousIndex: index, currentIndex } as CdkDragDrop); + await this.liveAnnouncer.announce( + this.i18nService.t("reorderFieldUp", label, currentIndex + 1, this.fields.length), + "assertive", + ); + + // Refocus the button after the reorder + // Angular re-renders the list when moving an item up which causes the focus to be lost + // Wait for the next tick to ensure the button is rendered before focusing + setTimeout(() => { + (event.target as HTMLButtonElement).focus(); + }); + } + + if (event.key === "ArrowDown" && index !== this.fields.length - 1) { + event.preventDefault(); + + const currentIndex = index + 1; + this.drop({ previousIndex: index, currentIndex } as CdkDragDrop); + await this.liveAnnouncer.announce( + this.i18nService.t("reorderFieldDown", label, currentIndex + 1, this.fields.length), + "assertive", + ); + } + } + + /** Create `FieldView` from the form objects and update the cipher */ + private updateCipher(fields: CustomField[]) { + const newFields = fields.map((field: CustomField) => { + let value: string; + + if (typeof field.value === "number") { + value = `${field.value}`; + } else if (typeof field.value === "boolean") { + value = field.value ? "true" : "false"; + } else { + value = field.value; + } + + const fieldView = new FieldView(); + fieldView.type = field.type; + fieldView.name = field.name; + fieldView.value = value; + fieldView.linkedId = field.linkedId; + return fieldView; + }); + + this.hasCustomFields = newFields.length > 0; + + this.numberOfFieldsChange.emit(newFields.length); + + this.cipherFormContainer.patchCipher({ + fields: newFields, + }); + } +} diff --git a/libs/vault/src/cipher-form/components/identity/identity.component.html b/libs/vault/src/cipher-form/components/identity/identity.component.html index f55397ce464..09abfb6b157 100644 --- a/libs/vault/src/cipher-form/components/identity/identity.component.html +++ b/libs/vault/src/cipher-form/components/identity/identity.component.html @@ -53,14 +53,26 @@ {{ "ssn" | i18n }} - +
{{ "passportNumber" | i18n }} - + diff --git a/libs/vault/src/cipher-form/components/identity/identity.component.ts b/libs/vault/src/cipher-form/components/identity/identity.component.ts index c1f44503610..a5545815e30 100644 --- a/libs/vault/src/cipher-form/components/identity/identity.component.ts +++ b/libs/vault/src/cipher-form/components/identity/identity.component.ts @@ -15,6 +15,7 @@ import { FormFieldModule, IconButtonModule, SelectModule, + TypographyModule, } from "@bitwarden/components"; import { CipherFormContainer } from "../../cipher-form-container"; @@ -34,6 +35,7 @@ import { CipherFormContainer } from "../../cipher-form-container"; FormFieldModule, IconButtonModule, SelectModule, + TypographyModule, ], }) export class IdentitySectionComponent implements OnInit { diff --git a/libs/vault/src/cipher-view/additional-information/additional-information.component.ts b/libs/vault/src/cipher-view/additional-information/additional-information.component.ts index a9660f3fc27..9e1376a8066 100644 --- a/libs/vault/src/cipher-view/additional-information/additional-information.component.ts +++ b/libs/vault/src/cipher-view/additional-information/additional-information.component.ts @@ -8,6 +8,7 @@ import { InputModule, SectionComponent, SectionHeaderComponent, + TypographyModule, } from "@bitwarden/components"; @Component({ @@ -22,6 +23,7 @@ import { InputModule, SectionComponent, SectionHeaderComponent, + TypographyModule, ], }) export class AdditionalInformationComponent { diff --git a/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts b/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts index c274fa4e9ac..6ea96bec497 100644 --- a/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts +++ b/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts @@ -22,6 +22,7 @@ import { IconButtonModule, SectionComponent, SectionHeaderComponent, + TypographyModule, } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -36,6 +37,7 @@ import { PasswordRepromptService } from "@bitwarden/vault"; IconButtonModule, SectionComponent, SectionHeaderComponent, + TypographyModule, ], }) export class AttachmentsV2Component { diff --git a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts index a40bca2d261..e54e5996eb5 100644 --- a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts +++ b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts @@ -12,6 +12,7 @@ import { InputModule, SectionComponent, SectionHeaderComponent, + TypographyModule, } from "@bitwarden/components"; @Component({ @@ -27,6 +28,7 @@ import { InputModule, SectionComponent, SectionHeaderComponent, + TypographyModule, ], }) export class CustomFieldV2Component { diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.html b/libs/vault/src/cipher-view/item-details/item-details-v2.component.html index 0ade00679af..6941c25c2e3 100644 --- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.html +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.html @@ -7,27 +7,32 @@ - +
-
+

{{ "ownerYou" | i18n }} -

-
+

{{ organization.name }} -

-
-

+

+
    +

    {{ collection.name }} -

-
-
+

+ +

{{ folder.name }} -

+

diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts index b0d158c1409..f4f60dc6f54 100644 --- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts @@ -6,13 +6,25 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; -import { CardComponent, SectionComponent, SectionHeaderComponent } from "@bitwarden/components"; +import { + CardComponent, + SectionComponent, + SectionHeaderComponent, + TypographyModule, +} from "@bitwarden/components"; @Component({ selector: "app-item-details-v2", templateUrl: "item-details-v2.component.html", standalone: true, - imports: [CommonModule, JslibModule, CardComponent, SectionComponent, SectionHeaderComponent], + imports: [ + CommonModule, + JslibModule, + CardComponent, + SectionComponent, + SectionHeaderComponent, + TypographyModule, + ], }) export class ItemDetailsV2Component { @Input() cipher: CipherView; diff --git a/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts b/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts index 51badfdbc89..830e37da61e 100644 --- a/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts +++ b/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts @@ -4,7 +4,12 @@ import { RouterModule } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { CardComponent, SectionComponent, SectionHeaderComponent } from "@bitwarden/components"; +import { + CardComponent, + SectionComponent, + SectionHeaderComponent, + TypographyModule, +} from "@bitwarden/components"; @Component({ selector: "app-item-history-v2", @@ -17,6 +22,7 @@ import { CardComponent, SectionComponent, SectionHeaderComponent } from "@bitwar CardComponent, SectionComponent, SectionHeaderComponent, + TypographyModule, ], }) export class ItemHistoryV2Component { diff --git a/libs/vault/src/components/assign-collections.component.html b/libs/vault/src/components/assign-collections.component.html new file mode 100644 index 00000000000..5663a9e817b --- /dev/null +++ b/libs/vault/src/components/assign-collections.component.html @@ -0,0 +1,42 @@ +
+

{{ "bulkCollectionAssignmentDialogDescription" | i18n }}

+ +
    +
  • +

    + {{ "bulkCollectionAssignmentWarning" | i18n: totalItemCount : readonlyItemCount }} +

    +
  • +
  • +

    + {{ transferWarningText(orgName, personalItemsCount) }} +

    +
  • +
+ +
+ + {{ "moveToOrganization" | i18n }} + + + + + +
+ +
+ + {{ "selectCollectionsToAssign" | i18n }} + + +
+
diff --git a/libs/vault/src/components/assign-collections.component.ts b/libs/vault/src/components/assign-collections.component.ts new file mode 100644 index 00000000000..bff7634dee2 --- /dev/null +++ b/libs/vault/src/components/assign-collections.component.ts @@ -0,0 +1,451 @@ +import { CommonModule } from "@angular/common"; +import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from "@angular/core"; +import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; +import { + Observable, + Subject, + combineLatest, + map, + shareReplay, + switchMap, + takeUntil, + tap, +} from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { PluralizePipe } from "@bitwarden/angular/pipes/pluralize.pipe"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { 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 { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; +import { + AsyncActionsModule, + BitSubmitDirective, + ButtonComponent, + ButtonModule, + DialogModule, + FormFieldModule, + MultiSelectModule, + SelectItemView, + SelectModule, + ToastService, +} from "@bitwarden/components"; + +export interface CollectionAssignmentParams { + organizationId: OrganizationId; + + /** + * The ciphers to be assigned to the collections selected in the dialog. + */ + ciphers: CipherView[]; + + /** + * The collections available to assign the ciphers to. + */ + availableCollections: CollectionView[]; + + /** + * The currently filtered collection. Selected by default. If the user deselects it in the dialog then it will be + * removed from the ciphers upon submission. + */ + activeCollection?: CollectionView; +} + +export enum CollectionAssignmentResult { + Saved = "saved", + Canceled = "canceled", +} + +const MY_VAULT_ID = "MyVault"; + +@Component({ + selector: "assign-collections", + templateUrl: "assign-collections.component.html", + standalone: true, + imports: [ + CommonModule, + JslibModule, + FormFieldModule, + AsyncActionsModule, + MultiSelectModule, + SelectModule, + ReactiveFormsModule, + ButtonModule, + DialogModule, + ], +}) +export class AssignCollectionsComponent implements OnInit { + @ViewChild(BitSubmitDirective) + private bitSubmit: BitSubmitDirective; + + @Input() params: CollectionAssignmentParams; + + /** + * Submit button instance that will be disabled or marked as loading when the form is submitting. + */ + @Input() submitBtn?: ButtonComponent; + + @Output() + editableItemCountChange = new EventEmitter(); + + @Output() onCollectionAssign = new EventEmitter(); + + formGroup = this.formBuilder.group({ + selectedOrg: [null], + collections: [[], [Validators.required]], + }); + + protected totalItemCount: number; + protected editableItemCount: number; + protected readonlyItemCount: number; + protected personalItemsCount: number; + protected availableCollections: SelectItemView[] = []; + protected orgName: string; + protected showOrgSelector: boolean = false; + + protected organizations$: Observable = + this.organizationService.organizations$.pipe( + map((orgs) => + orgs + .filter((o) => o.enabled && o.status === OrganizationUserStatusType.Confirmed) + .sort((a, b) => a.name.localeCompare(b.name)), + ), + tap((orgs) => { + if (orgs.length > 0 && this.showOrgSelector) { + // Using setTimeout to defer the patchValue call until the next event loop cycle + setTimeout(() => { + this.formGroup.patchValue({ selectedOrg: orgs[0].id }); + this.setFormValidators(); + }); + } + }), + ); + + protected transferWarningText = (orgName: string, itemsCount: number) => { + const pluralizedItems = this.pluralizePipe.transform(itemsCount, "item", "items"); + return orgName + ? this.i18nService.t("personalItemsWithOrgTransferWarning", pluralizedItems, orgName) + : this.i18nService.t("personalItemsTransferWarning", pluralizedItems); + }; + + private editableItems: CipherView[] = []; + // Get the selected organization ID. If the user has not selected an organization from the form, + // fallback to use the organization ID from the params. + private get selectedOrgId(): OrganizationId { + return this.formGroup.value.selectedOrg || this.params.organizationId; + } + private destroy$ = new Subject(); + + constructor( + private cipherService: CipherService, + private i18nService: I18nService, + private configService: ConfigService, + private organizationService: OrganizationService, + private collectionService: CollectionService, + private formBuilder: FormBuilder, + private pluralizePipe: PluralizePipe, + private toastService: ToastService, + ) {} + + async ngOnInit() { + const v1FCEnabled = await this.configService.getFeatureFlag(FeatureFlag.FlexibleCollectionsV1); + const restrictProviderAccess = await this.configService.getFeatureFlag( + FeatureFlag.RestrictProviderAccess, + ); + + const onlyPersonalItems = this.params.ciphers.every((c) => c.organizationId == null); + + if (this.selectedOrgId === MY_VAULT_ID || onlyPersonalItems) { + this.showOrgSelector = true; + } + + await this.initializeItems(this.selectedOrgId, v1FCEnabled, restrictProviderAccess); + + if (this.selectedOrgId && this.selectedOrgId !== MY_VAULT_ID) { + await this.handleOrganizationCiphers(); + } + + this.setupFormSubscriptions(); + } + + ngAfterViewInit(): void { + this.bitSubmit.loading$.pipe(takeUntil(this.destroy$)).subscribe((loading) => { + if (!this.submitBtn) { + return; + } + + this.submitBtn.loading = loading; + }); + + this.bitSubmit.disabled$.pipe(takeUntil(this.destroy$)).subscribe((disabled) => { + if (!this.submitBtn) { + return; + } + + this.submitBtn.disabled = disabled; + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + selectCollections(items: SelectItemView[]) { + const currentCollections = this.formGroup.controls.collections.value as SelectItemView[]; + const updatedCollections = [...currentCollections, ...items].sort(this.sortItems); + this.formGroup.patchValue({ collections: updatedCollections }); + } + + submit = async () => { + this.formGroup.markAllAsTouched(); + + if (this.formGroup.invalid) { + return; + } + + // Retrieve ciphers that belong to an organization + const cipherIds = this.editableItems + .filter((i) => i.organizationId) + .map((i) => i.id as CipherId); + + // Move personal items to the organization + if (this.personalItemsCount > 0) { + await this.moveToOrganization( + this.selectedOrgId, + this.params.ciphers.filter((c) => c.organizationId == null), + this.formGroup.controls.collections.value.map((i) => i.id as CollectionId), + ); + } + + if (cipherIds.length > 0) { + const isSingleOrgCipher = cipherIds.length === 1 && this.personalItemsCount === 0; + + // Update assigned collections for single org cipher or bulk update collections for multiple org ciphers + await (isSingleOrgCipher + ? this.updateAssignedCollections(this.editableItems[0]) + : this.bulkUpdateCollections(cipherIds)); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("successfullyAssignedCollections"), + }); + } + + this.onCollectionAssign.emit(CollectionAssignmentResult.Saved); + }; + + private sortItems = (a: SelectItemView, b: SelectItemView) => + this.i18nService.collator.compare(a.labelName, b.labelName); + + private async handleOrganizationCiphers() { + // If no ciphers are editable, cancel the operation + if (this.editableItemCount == 0) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("nothingSelected"), + }); + this.onCollectionAssign.emit(CollectionAssignmentResult.Canceled); + + return; + } + + this.availableCollections = this.params.availableCollections.map((c) => ({ + icon: "bwi-collection", + id: c.id, + labelName: c.name, + listName: c.name, + })); + + // Select assigned collections for a single cipher. + this.selectCollectionsAssignedToSingleCipher(); + + // If the active collection is set, select it by default + if (this.params.activeCollection) { + this.selectCollections([ + { + icon: "bwi-collection", + id: this.params.activeCollection.id, + labelName: this.params.activeCollection.name, + listName: this.params.activeCollection.name, + }, + ]); + } + } + + /** + * Selects the collections that are assigned to a single cipher, + * excluding the active collection. + */ + private selectCollectionsAssignedToSingleCipher() { + if (this.params.ciphers.length !== 1) { + return; + } + + const assignedCollectionIds = this.params.ciphers[0].collectionIds; + + // Filter the available collections to select only those that are associated with the ciphers, excluding the active collection + const assignedCollections = this.availableCollections + .filter( + (collection) => + assignedCollectionIds.includes(collection.id) && + collection.id !== this.params.activeCollection?.id, + ) + .map((collection) => ({ + icon: "bwi-collection", + id: collection.id, + labelName: collection.labelName, + listName: collection.listName, + })); + + if (assignedCollections.length > 0) { + this.selectCollections(assignedCollections); + } + } + + private async initializeItems( + organizationId: OrganizationId, + v1FCEnabled: boolean, + restrictProviderAccess: boolean, + ) { + this.totalItemCount = this.params.ciphers.length; + + // If organizationId is not present or organizationId is MyVault, then all ciphers are considered personal items + if (!organizationId || organizationId === MY_VAULT_ID) { + this.editableItems = this.params.ciphers; + this.editableItemCount = this.params.ciphers.length; + this.personalItemsCount = this.params.ciphers.length; + this.editableItemCountChange.emit(this.editableItemCount); + return; + } + + const org = await this.organizationService.get(organizationId); + this.orgName = org.name; + + this.editableItems = org.canEditAllCiphers(v1FCEnabled, restrictProviderAccess) + ? this.params.ciphers + : this.params.ciphers.filter((c) => c.edit); + + this.editableItemCount = this.editableItems.length; + // TODO: https://bitwarden.atlassian.net/browse/PM-9307, + // clean up editableItemCountChange when the org vault is updated to filter editable ciphers + this.editableItemCountChange.emit(this.editableItemCount); + this.personalItemsCount = this.params.ciphers.filter((c) => c.organizationId == null).length; + this.readonlyItemCount = this.totalItemCount - this.editableItemCount; + } + + private setFormValidators() { + const selectedOrgControl = this.formGroup.get("selectedOrg"); + selectedOrgControl?.setValidators([Validators.required]); + selectedOrgControl?.updateValueAndValidity(); + } + + /** + * Sets up form subscriptions for selected organizations. + */ + private setupFormSubscriptions() { + // Listen to changes in selected organization and update collections + this.formGroup.controls.selectedOrg.valueChanges + .pipe( + tap(() => { + this.formGroup.controls.collections.setValue([], { emitEvent: false }); + }), + switchMap((orgId) => { + return this.getCollectionsForOrganization(orgId as OrganizationId); + }), + takeUntil(this.destroy$), + ) + .subscribe((collections) => { + this.availableCollections = collections.map((c) => ({ + icon: "bwi-collection", + id: c.id, + labelName: c.name, + listName: c.name, + })); + }); + } + + /** + * Retrieves the collections for the organization with the given ID. + * @param orgId + * @returns An observable of the collections for the organization. + */ + private getCollectionsForOrganization(orgId: OrganizationId): Observable { + return combineLatest([ + this.collectionService.decryptedCollections$, + this.organizationService.organizations$, + ]).pipe( + map(([collections, organizations]) => { + const org = organizations.find((o) => o.id === orgId); + this.orgName = org.name; + + return collections.filter((c) => { + return c.organizationId === orgId && !c.readOnly; + }); + }), + shareReplay({ refCount: true, bufferSize: 1 }), + ); + } + + private async moveToOrganization( + organizationId: OrganizationId, + shareableCiphers: CipherView[], + selectedCollectionIds: CollectionId[], + ) { + await this.cipherService.shareManyWithServer( + shareableCiphers, + organizationId, + selectedCollectionIds, + ); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t( + "movedItemsToOrg", + this.orgName ?? this.i18nService.t("organization"), + ), + }); + } + + private async bulkUpdateCollections(cipherIds: CipherId[]) { + if (this.formGroup.controls.collections.value.length > 0) { + await this.cipherService.bulkUpdateCollectionsWithServer( + this.selectedOrgId, + cipherIds, + this.formGroup.controls.collections.value.map((i) => i.id as CollectionId), + false, + ); + } + + if ( + this.params.activeCollection != null && + this.formGroup.controls.collections.value.find( + (c) => c.id === this.params.activeCollection.id, + ) == null + ) { + await this.cipherService.bulkUpdateCollectionsWithServer( + this.selectedOrgId, + cipherIds, + [this.params.activeCollection.id as CollectionId], + true, + ); + } + } + + private async updateAssignedCollections(cipherView: CipherView) { + const { collections } = this.formGroup.getRawValue(); + cipherView.collectionIds = collections.map((i) => i.id as CollectionId); + const cipher = await this.cipherService.encrypt(cipherView); + await this.cipherService.saveCollectionsWithServer(cipher); + } +} diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index e4e17e7aa5a..5dee70ea46f 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -4,3 +4,8 @@ export { CopyCipherFieldDirective } from "./components/copy-cipher-field.directi export * from "./cipher-view"; export * from "./cipher-form"; +export { + AssignCollectionsComponent, + CollectionAssignmentParams, + CollectionAssignmentResult, +} from "./components/assign-collections.component"; diff --git a/libs/vault/src/services/password-reprompt.service.ts b/libs/vault/src/services/password-reprompt.service.ts index 8621c436bae..6583d0787fc 100644 --- a/libs/vault/src/services/password-reprompt.service.ts +++ b/libs/vault/src/services/password-reprompt.service.ts @@ -1,7 +1,8 @@ import { Injectable } from "@angular/core"; -import { lastValueFrom } from "rxjs"; +import { firstValueFrom, lastValueFrom } from "rxjs"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherRepromptType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; @@ -19,6 +20,10 @@ export class PasswordRepromptService { private userVerificationService: UserVerificationService, ) {} + enabled$ = Utils.asyncToObservable(() => + this.userVerificationService.hasMasterPasswordAndMasterKeyHash(), + ); + protectedFields() { return ["TOTP", "Password", "H_Field", "Card Number", "Security Code"]; } @@ -45,7 +50,7 @@ export class PasswordRepromptService { return result === true; } - async enabled() { - return await this.userVerificationService.hasMasterPasswordAndMasterKeyHash(); + enabled() { + return firstValueFrom(this.enabled$); } } diff --git a/package-lock.json b/package-lock.json index 6888df5e686..ef366f224d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -117,8 +117,8 @@ "@types/react": "16.14.60", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.4", - "@typescript-eslint/eslint-plugin": "7.13.1", - "@typescript-eslint/parser": "7.13.1", + "@typescript-eslint/eslint-plugin": "7.16.1", + "@typescript-eslint/parser": "7.16.1", "@webcomponents/custom-elements": "1.6.0", "@yao-pkg/pkg": "^5.12.0", "autoprefixer": "10.4.19", @@ -175,7 +175,7 @@ "storybook": "7.6.19", "style-loader": "3.3.4", "tailwindcss": "3.4.3", - "ts-jest": "29.1.5", + "ts-jest": "29.2.2", "ts-loader": "9.5.1", "tsconfig-paths-webpack-plugin": "4.1.0", "type-fest": "2.19.0", @@ -183,7 +183,7 @@ "url": "0.11.3", "util": "0.12.5", "wait-on": "7.2.0", - "webpack": "5.92.0", + "webpack": "5.93.0", "webpack-cli": "5.1.4", "webpack-dev-server": "5.0.4", "webpack-node-externals": "3.0.0" @@ -195,11 +195,11 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2024.7.0" + "version": "2024.7.1" }, "apps/cli": { "name": "@bitwarden/cli", - "version": "2024.7.0", + "version": "2024.7.1", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@koa/multer": "3.0.2", @@ -235,7 +235,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.7.1", + "version": "2024.7.2", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -249,7 +249,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2024.7.0" + "version": "2024.7.1" }, "libs/admin-console": { "name": "@bitwarden/admin-console", @@ -12087,17 +12087,17 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.13.1.tgz", - "integrity": "sha512-kZqi+WZQaZfPKnsflLJQCz6Ze9FFSMfXrrIOcyargekQxG37ES7DJNpJUE9Q/X5n3yTIP/WPutVNzgknQ7biLg==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.1.tgz", + "integrity": "sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.13.1", - "@typescript-eslint/type-utils": "7.13.1", - "@typescript-eslint/utils": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/scope-manager": "7.16.1", + "@typescript-eslint/type-utils": "7.16.1", + "@typescript-eslint/utils": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -12121,14 +12121,14 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.13.1.tgz", - "integrity": "sha512-aWDbLu1s9bmgPGXSzNCxELu+0+HQOapV/y+60gPXafR8e2g1Bifxzevaa+4L2ytCWm+CHqpELq4CSoN9ELiwCg==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.1.tgz", + "integrity": "sha512-rbu/H2MWXN4SkjIIyWcmYBjlp55VT+1G3duFOIukTNFxr9PI35pLc2ydwAfejCEitCv4uztA07q0QWanOHC7dA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.13.1", - "@typescript-eslint/utils": "7.13.1", + "@typescript-eslint/typescript-estree": "7.16.1", + "@typescript-eslint/utils": "7.16.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -12149,16 +12149,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.13.1.tgz", - "integrity": "sha512-h5MzFBD5a/Gh/fvNdp9pTfqJAbuQC4sCN2WzuXme71lqFJsZtLbjxfSk4r3p02WIArOF9N94pdsLiGutpDbrXQ==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.1.tgz", + "integrity": "sha512-WrFM8nzCowV0he0RlkotGDujx78xudsxnGMBHI88l5J8wEhED6yBwaSLP99ygfrzAjsQvcYQ94quDwI0d7E1fA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.13.1", - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/typescript-estree": "7.13.1" + "@typescript-eslint/scope-manager": "7.16.1", + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/typescript-estree": "7.16.1" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -12191,16 +12191,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.13.1.tgz", - "integrity": "sha512-1ELDPlnLvDQ5ybTSrMhRTFDfOQEOXNM+eP+3HT/Yq7ruWpciQw+Avi73pdEbA4SooCawEWo3dtYbF68gN7Ed1A==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.1.tgz", + "integrity": "sha512-u+1Qx86jfGQ5i4JjK33/FnawZRpsLxRnKzGE6EABZ40KxVT/vWsiZFEBBHjFOljmmV3MBYOHEKi0Jm9hbAOClA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "7.13.1", - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/typescript-estree": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/scope-manager": "7.16.1", + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/typescript-estree": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1", "debug": "^4.3.4" }, "engines": { @@ -12220,14 +12220,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.1.tgz", - "integrity": "sha512-adbXNVEs6GmbzaCpymHQ0MB6E4TqoiVbC0iqG3uijR8ZYfpAXMGttouQzF4Oat3P2GxDVIrg7bMI/P65LiQZdg==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.1.tgz", + "integrity": "sha512-nYpyv6ALte18gbMz323RM+vpFpTjfNdyakbf3nsLvF43uF9KeNC289SUEW3QLZ1xPtyINJ1dIsZOuWuSRIWygw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1" + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -12322,9 +12322,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.1.tgz", - "integrity": "sha512-7K7HMcSQIAND6RBL4kDl24sG/xKM13cA85dc7JnmQXw2cBDngg7c19B++JzvJHRG3zG36n9j1i451GBzRuHchw==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz", + "integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==", "dev": true, "license": "MIT", "engines": { @@ -12336,14 +12336,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.1.tgz", - "integrity": "sha512-uxNr51CMV7npU1BxZzYjoVz9iyjckBduFBP0S5sLlh1tXYzHzgZ3BR9SVsNed+LmwKrmnqN3Kdl5t7eZ5TS1Yw==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.1.tgz", + "integrity": "sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -12520,13 +12520,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.1.tgz", - "integrity": "sha512-k/Bfne7lrP7hcb7m9zSsgcBmo+8eicqqfNAJ7uUY+jkTFpKeH2FSkWpFRtimBxgkyvqfu9jTPRbYOvud6isdXA==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.1.tgz", + "integrity": "sha512-Qlzzx4sE4u3FsHTPQAAQFJFNOuqtuY0LFrZHwQ8IHK705XxBiWOFkfKRWu6niB7hwfgnwIpO4jTC75ozW1PHWg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.13.1", + "@typescript-eslint/types": "7.16.1", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -37630,13 +37630,14 @@ "dev": true }, "node_modules/ts-jest": { - "version": "29.1.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.5.tgz", - "integrity": "sha512-UuClSYxM7byvvYfyWdFI+/2UxMmwNyJb0NPkZPQE2hew3RurV7l7zURgOHAd/1I1ZdPpe3GUsXNXAcN8TFKSIg==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.2.tgz", + "integrity": "sha512-sSW7OooaKT34AAngP6k1VS669a0HdLxkQZnlC7T76sckGCokXFnvJ3yRlQZGRTAoV5K19HfSgCiSwWOSIfcYlg==", "dev": true, "license": "MIT", "dependencies": { "bs-logger": "0.x", + "ejs": "^3.0.0", "fast-json-stable-stringify": "2.x", "jest-util": "^29.0.0", "json5": "^2.2.3", @@ -39346,9 +39347,9 @@ } }, "node_modules/webpack": { - "version": "5.92.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.0.tgz", - "integrity": "sha512-Bsw2X39MYIgxouNATyVpCNVWBCuUwDgWtN78g6lSdPJRLaQ/PUVm/oXcaRAyY/sMFoKFQrsPeqvTizWtq7QPCA==", + "version": "5.93.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", + "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 01ded518804..ed8ebcef2f6 100644 --- a/package.json +++ b/package.json @@ -79,8 +79,8 @@ "@types/react": "16.14.60", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.4", - "@typescript-eslint/eslint-plugin": "7.13.1", - "@typescript-eslint/parser": "7.13.1", + "@typescript-eslint/eslint-plugin": "7.16.1", + "@typescript-eslint/parser": "7.16.1", "@webcomponents/custom-elements": "1.6.0", "@yao-pkg/pkg": "^5.12.0", "autoprefixer": "10.4.19", @@ -137,7 +137,7 @@ "storybook": "7.6.19", "style-loader": "3.3.4", "tailwindcss": "3.4.3", - "ts-jest": "29.1.5", + "ts-jest": "29.2.2", "ts-loader": "9.5.1", "tsconfig-paths-webpack-plugin": "4.1.0", "type-fest": "2.19.0", @@ -145,7 +145,7 @@ "url": "0.11.3", "util": "0.12.5", "wait-on": "7.2.0", - "webpack": "5.92.0", + "webpack": "5.93.0", "webpack-cli": "5.1.4", "webpack-dev-server": "5.0.4", "webpack-node-externals": "3.0.0"