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