diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 17a1cb5720e..9502a9c404d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -18,6 +18,7 @@ apps/cli/src/auth @bitwarden/team-auth-dev apps/desktop/src/auth @bitwarden/team-auth-dev apps/web/src/app/auth @bitwarden/team-auth-dev libs/auth @bitwarden/team-auth-dev +libs/user-core @bitwarden/team-auth-dev # web connectors used for auth apps/web/src/connectors @bitwarden/team-auth-dev bitwarden_license/bit-web/src/app/auth @bitwarden/team-auth-dev @@ -91,6 +92,8 @@ libs/common/spec @bitwarden/team-platform-dev libs/common/src/state-migrations @bitwarden/team-platform-dev libs/platform @bitwarden/team-platform-dev libs/storage-core @bitwarden/team-platform-dev +libs/logging @bitwarden/team-platform-dev +libs/storage-test-utils @bitwarden/team-platform-dev # Web utils used across app and connectors apps/web/src/utils/ @bitwarden/team-platform-dev # Web core and shared files diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index ea113f8b9a5..c75298a3e92 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -268,6 +268,29 @@ jobs: working-directory: browser-source/ run: npm link ../sdk-internal + - name: Check source file size + if: ${{ startsWith(matrix.name, 'firefox') }} + run: | + # Declare variable as indexed array + declare -a FILES + + # Search for source files that are greater than 4M + TARGET_DIR='./browser-source/apps/browser' + while IFS=' ' read -r RESULT; do + FILES+=("$RESULT") + done < <(find $TARGET_DIR -size +4M) + + # Validate results and provide messaging + if [[ ${#FILES[@]} -ne 0 ]]; then + echo "File(s) exceeds size limit: 4MB" + for FILE in ${FILES[@]}; do + echo "- $(du --si $FILE)" + done + echo "ERROR Firefox rejects extension uploads that contain files larger than 4MB" + # Invoke failure + exit 1 + fi + - name: Build extension run: npm run ${{ matrix.npm_command }} working-directory: browser-source/apps/browser diff --git a/apps/browser/package.json b/apps/browser/package.json index 9b6d0174b0f..16e460a9025 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2025.6.0", + "version": "2025.6.1", "scripts": { "build": "npm run build:chrome", "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 6c7f7f0fccd..c56ea69d862 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "يجب عليك تأكيد بريدك الإلكتروني لاستخدام هذه الميزة. يمكنك تأكيد بريدك الإلكتروني في خزانة الويب." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "تحديث كلمة المرور الرئيسية" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index ddf82f37d2f..f63038beb67 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Bu özəlliyi istifadə etmək üçün e-poçtunuzu doğrulamalısınız. E-poçtunuzu veb seyfdə doğrulaya bilərsiniz." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Güncəllənmiş ana parol" }, @@ -4244,6 +4247,26 @@ "message": "Ortaq formatlar", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Brauzer ayarları ilə davam edilsin?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 3944569df94..2a691bc4987 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Вы павінны праверыць свой адрас электроннай пошты, каб выкарыстоўваць гэту функцыю. Зрабіць гэта можна ў вэб-сховішчы." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Асноўны пароль абноўлены" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 53e896e6a72..fa822d424ff 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Трябва да потвърдите е-пощата си, за да използвате тази функционалност. Можете да го направите в уеб-трезора." }, + "masterPasswordSuccessfullySet": { + "message": "Главната парола е зададена успешно" + }, "updatedMasterPassword": { "message": "Главната парола е променена" }, @@ -4244,6 +4247,26 @@ "message": "Често използвани формати", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "Разпознаването на съвпадения чрез адреса е начинът, по който Битуорден определя кои елементи да предложи за автоматично попълване.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "„Регулярен израз“ е по-сложен метод, който е и по-рисков, тъй като може да застраши сигурността на данните за вписване.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "„Започва с“ е по-сложен метод, който е и по-рисков, тъй като може да застраши сигурността на данните за вписване.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "Повече относно разпознаването на съвпадения", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Разширени настройки", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Продължаване към настройките на браузъра?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" @@ -5098,7 +5121,7 @@ "description": "Represents the ~ key in screen reader content as a readable word" }, "backtickCharacterDescriptor": { - "message": "Backtick", + "message": "Обратен апостроф", "description": "Represents the ` key in screen reader content as a readable word" }, "exclamationCharacterDescriptor": { diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 29e0e3800c8..48c0dbef228 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "প্রধান পাসওয়ার্ড আপডেট করা হয়েছে" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 7c575cdb72b..83228e94611 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Updated master password" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index ff3226255e3..edc139b1f23 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -2919,6 +2919,9 @@ "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." }, + "masterPasswordSuccessfullySet": { + "message": "La contrasenya mestra s'ha configurat correctament" + }, "updatedMasterPassword": { "message": "Contrasenya mestra actualitzada" }, @@ -4244,6 +4247,26 @@ "message": "Formats comuns", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Voleu continuar a la configuració del navegador?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 52c548a93b0..a9c4a823109 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -2919,6 +2919,9 @@ "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." }, + "masterPasswordSuccessfullySet": { + "message": "Hlavní heslo bylo úspěšně nastaveno" + }, "updatedMasterPassword": { "message": "Hlavní heslo bylo aktualizováno" }, @@ -4244,6 +4247,26 @@ "message": "Společné formáty", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "Detekce shody URI je způsob, jakým Bitwarden identifikuje návrhy automatického vyplňování.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regulární výraz\" je pokročilá volba se zvýšeným rizikem odhalení přihlašovacích údajů.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Začíná na\" je pokročilá volba se zvýšeným rizikem odhalení přihlašovacích údajů.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "Další informace o detekci shody", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Rozšířené volby", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Pokračovat do nastavení prohlížeče?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index c060d81bcfc..6f0ce7a438a 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Diweddarwyd y prif gyfrinair" }, @@ -4244,6 +4247,26 @@ "message": "Fformatau cyffredin", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 1b79fc0ecf9..ebd10e7b701 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Du skal bekræfte din e-mail for at bruge denne funktion. Du kan bekræfte din e-mail i web-boksen." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Hovedadgangskode opdateret" }, @@ -4244,6 +4247,26 @@ "message": "Almindelige formater", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Fortsæt til Browserindstillinger?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 418c15dc3a0..8822f346e88 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -1060,7 +1060,7 @@ "message": "Soll Bitwarden sich dieses Passwort merken?" }, "notificationAddSave": { - "message": "Ja, jetzt speichern" + "message": "Speichern" }, "notificationViewAria": { "message": "$ITEMNAME$ anzeigen, öffnet sich in neuem Fenster", @@ -2919,6 +2919,9 @@ "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." }, + "masterPasswordSuccessfullySet": { + "message": "Master-Passwort erfolgreich eingerichtet" + }, "updatedMasterPassword": { "message": "Master-Passwort aktualisiert" }, @@ -4244,6 +4247,26 @@ "message": "Gängigste Formate", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Weiter zu den Browsereinstellungen?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index acd7cac0c9b..4779fcf021c 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Πρέπει να επαληθεύσετε το email σας για να χρησιμοποιήσετε αυτή τη δυνατότητα. Μπορείτε να επαληθεύσετε το email σας στο web vault." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Ενημερώθηκε ο κύριος κωδικός πρόσβασης" }, @@ -4244,6 +4247,26 @@ "message": "Κοινοί τύποι", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Συνεχίστε στις ρυθμίσεις περιηγητή;", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index b6a8d1834b4..996bdcdae79 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Updated master password" }, @@ -4243,6 +4246,26 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption":{ + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index ffca2486ff4..dddae654d96 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Updated master password" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 02d2ba35060..deaadf4f900 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Updated Master Password" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 250aa3430e0..7dc607d30d0 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Debes verificar tu correo electrónico para usar esta función. Puedes verificar tu correo electrónico en la caja fuerte web." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Contraseña maestra actualizada" }, @@ -4244,6 +4247,26 @@ "message": "Formatos comunes", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "¿Continuar a los ajustes del navegador?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 6893dec3f4f..a5e53be45cb 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Selle funktsiooni kasutamiseks pead kinnitama oma e-posti aadressi. Saad seda teha veebihoidlas." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Uuendas ülemparooli" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 49e87abcf83..3c9c83777ad 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Emaila egiaztatu behar duzu funtzio hau erabiltzeko. Emaila web-eko kutxa gotorrean egiazta dezakezu." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Pasahitz nagusia eguneratuta" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index a1979d703bf..77c9da484f2 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "برای استفاده از این ویژگی باید ایمیل خود را تأیید کنید. می‌توانید ایمیل خود را در گاوصندوق وب تأیید کنید." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "کلمه عبور اصلی به‌روزرسانی شد" }, @@ -4244,6 +4247,26 @@ "message": "فرمت‌های رایج", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "ادامه به تنظیمات مرورگر؟", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 098e361223a..d7484b49040 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Sinun on vahvistettava sähköpostiosoitteesi käyttääksesi ominaisuutta. Voit vahvistaa osoitteesi verkkoholvissa." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Pääsalasanasi on vaihdettu" }, @@ -4244,6 +4247,26 @@ "message": "Yleiset muodot", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Avataanko selaimen asetukset?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index de17e87a57b..260ce635a1a 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -2919,6 +2919,9 @@ "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." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "I-update ang master password" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index e1397980675..ab449fc07af 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -3,7 +3,7 @@ "message": "Bitwarden" }, "appLogoLabel": { - "message": "Bitwarden logo" + "message": "Logo de Bitwarden" }, "extName": { "message": "Gestionnaire de mots de passe Bitwarden", @@ -887,7 +887,7 @@ "message": "Suivez les étapes ci-dessous afin de réussir à vous connecter." }, "followTheStepsBelowToFinishLoggingInWithSecurityKey": { - "message": "Follow the steps below to finish logging in with your security key." + "message": "Suivez les étapes ci-dessous pour terminer la connexion avec votre clé de sécurité." }, "restartRegistration": { "message": "Redémarrer l'inscription" @@ -1080,7 +1080,7 @@ "description": "Tooltip and Aria label for edit button on cipher item" }, "newNotification": { - "message": "New notification" + "message": "Nouvelle notification" }, "labelWithNotification": { "message": "$LABEL$: New notification", @@ -1093,11 +1093,11 @@ } }, "notificationLoginSaveConfirmation": { - "message": "saved to Bitwarden.", + "message": "enregistré dans Bitwarden.", "description": "Shown to user after item is saved." }, "notificationLoginUpdatedConfirmation": { - "message": "updated in Bitwarden.", + "message": "mis à jour dans Bitwarden.", "description": "Shown to user after item is updated." }, "selectItemAriaLabel": { @@ -1129,7 +1129,7 @@ "description": "Prompt asking the user if they want to save their login details." }, "updateLogin": { - "message": "Update existing login", + "message": "Mettre à jour de l'identifiant existant", "description": "Prompt asking the user if they want to update an existing login entry." }, "loginSaveSuccess": { @@ -1162,7 +1162,7 @@ "description": "Shown to user after login is updated." }, "nextSecurityTaskAction": { - "message": "Change next password", + "message": "Changer le mot de passe suivant", "description": "Message prompting user to undertake completion of another security task." }, "saveFailure": { @@ -2525,7 +2525,7 @@ "message": "Modifier" }, "changePassword": { - "message": "Change password", + "message": "Changer le mot de passe", "description": "Change password button for browser at risk notification on login." }, "changeButtonTitle": { @@ -2538,7 +2538,7 @@ } }, "atRiskPassword": { - "message": "At-risk password" + "message": "Mot de passe à risque" }, "atRiskPasswords": { "message": "Mots de passe à risque" @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Vous devez vérifier votre courriel pour utiliser cette fonctionnalité. Vous pouvez vérifier votre courriel dans le coffre web." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Mot de passe principal mis à jour" }, @@ -4244,6 +4247,26 @@ "message": "Formats communs", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continuer vers les paramètres du navigateur ?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 152f4d7236c..06b4cd73bad 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Debes verificar o teu correo electrónico para empregar esta función. Podes facelo dende a aplicación web." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Contrasinal mestre actualizado" }, @@ -4244,6 +4247,26 @@ "message": "Formatos comúns", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Ir ós axustes do navegador?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 215aa17988d..5010fa8ac16 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "עליך לאמת את הדוא\"ל שלך כדי להשתמש בתכונה זו. ניתן לאמת את הדוא\"ל שלך בכספת הרשת." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "סיסמה ראשית עודכנה" }, @@ -4244,6 +4247,26 @@ "message": "פורמטים נפוצים", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "להמשיך אל הגדרות הדפדפן?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 64a98de313b..c33c6e249c6 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "इस सुविधा का उपयोग करने के लिए आपको अपने ईमेल को सत्यापित करना होगा। आप वेब वॉल्ट में अपने ईमेल को सत्यापित कर सकते हैं।" }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "अपडेट किया गया मास्टर पासवर्ड" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 69a68e921ae..2dfcd35da82 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -887,7 +887,7 @@ "message": "Prati korake za dovršetak prijave." }, "followTheStepsBelowToFinishLoggingInWithSecurityKey": { - "message": "Follow the steps below to finish logging in with your security key." + "message": "Prati korake za dovršetak prijave svojim sigurnosnim ključem." }, "restartRegistration": { "message": "Ponovno pokreni registraciju" @@ -1063,7 +1063,7 @@ "message": "Spremi" }, "notificationViewAria": { - "message": "View $ITEMNAME$, opens in new window", + "message": "Pogledaj $ITEMNAME$, otvara u novom prozoru", "placeholders": { "itemName": { "content": "$1" @@ -1072,18 +1072,18 @@ "description": "Aria label for the view button in notification bar confirmation message" }, "notificationNewItemAria": { - "message": "New Item, opens in new window", + "message": "Nova stavka, otvara u novom prozoru", "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" }, "notificationEditTooltip": { - "message": "Edit before saving", + "message": "Uredi prije spremanja", "description": "Tooltip and Aria label for edit button on cipher item" }, "newNotification": { - "message": "New notification" + "message": "Nova obavijest" }, "labelWithNotification": { - "message": "$LABEL$: New notification", + "message": "$LABEL$: Nova obavijest", "description": "Label for the notification with a new login suggestion.", "placeholders": { "label": { @@ -1093,15 +1093,15 @@ } }, "notificationLoginSaveConfirmation": { - "message": "saved to Bitwarden.", + "message": "spremljeno u Bitwarden.", "description": "Shown to user after item is saved." }, "notificationLoginUpdatedConfirmation": { - "message": "updated in Bitwarden.", + "message": "ažurirano u Bitwarenu.", "description": "Shown to user after item is updated." }, "selectItemAriaLabel": { - "message": "Select $ITEMTYPE$, $ITEMNAME$", + "message": "Odaberi $ITEMTYPE$, $ITEMNAME$", "description": "Used by screen readers. $1 is the item type (like vault or folder), $2 is the selected item name.", "placeholders": { "itemType": { @@ -1121,15 +1121,15 @@ "description": "Button text for updating an existing login entry." }, "unlockToSave": { - "message": "Unlock to save this login", + "message": "Otključaj za spremanje ove prijave", "description": "User prompt to take action in order to save the login they just entered." }, "saveLogin": { - "message": "Save login", + "message": "Spremi prijavu", "description": "Prompt asking the user if they want to save their login details." }, "updateLogin": { - "message": "Update existing login", + "message": "Ažuriraj postojeću prijavu", "description": "Prompt asking the user if they want to update an existing login entry." }, "loginSaveSuccess": { @@ -1141,7 +1141,7 @@ "description": "Message displayed when login details are successfully updated." }, "loginUpdateTaskSuccess": { - "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "message": "Odlično! Ti i $ORGANIZATION$ ste sada sigurniji.", "placeholders": { "organization": { "content": "$1" @@ -1150,7 +1150,7 @@ "description": "Shown to user after login is updated." }, "loginUpdateTaskSuccessAdditional": { - "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "message": "Hvala što je $ORGANIZATION$ sada sigurnija. Imaš još $TASK_COUNT$ lozinku za ažuriranje.", "placeholders": { "organization": { "content": "$1" @@ -1162,7 +1162,7 @@ "description": "Shown to user after login is updated." }, "nextSecurityTaskAction": { - "message": "Change next password", + "message": "Promjeni sljedeću lozinku", "description": "Message prompting user to undertake completion of another security task." }, "saveFailure": { @@ -1366,7 +1366,7 @@ "message": "Značajka nije dostupna" }, "legacyEncryptionUnsupported": { - "message": "Legacy encryption is no longer supported. Please contact support to recover your account." + "message": "Zastarjelo šifriranje više nije podržano. Obrati se podršci za oporavak računa." }, "premiumMembership": { "message": "Premium članstvo" @@ -1600,13 +1600,13 @@ "message": "Prijedlozi auto-ispune" }, "autofillSpotlightTitle": { - "message": "Easily find autofill suggestions" + "message": "Jednostavno pronađi prijedloge auto-ispune" }, "autofillSpotlightDesc": { - "message": "Turn off your browser's autofill settings, so they don't conflict with Bitwarden." + "message": "Isključi postavke auto-ispune preglenika kako se ne bi kosile s Bitwardenom." }, "turnOffBrowserAutofill": { - "message": "Turn off $BROWSER$ autofill", + "message": "Isključi auto-ispunu za $BROWSER$", "placeholders": { "browser": { "content": "$1", @@ -1615,7 +1615,7 @@ } }, "turnOffAutofill": { - "message": "Turn off autofill" + "message": "Isključi auto-ispunu" }, "showInlineMenuLabel": { "message": "Prikaži prijedloge auto-ispune na poljima obrazaca" @@ -1923,7 +1923,7 @@ "message": "SSH ključ" }, "typeNote": { - "message": "Note" + "message": "Bilješka" }, "newItemHeader": { "message": "Novi $TYPE$", @@ -2157,7 +2157,7 @@ "message": "Postavi svoj PIN kôd za otključavanje Bitwardena. Postavke PIN-a se resetiraju ako se potpuno odjaviš iz aplikacije." }, "setPinCode": { - "message": "You can use this PIN to unlock Bitwarden. Your PIN will be reset if you ever fully log out of the application." + "message": "Ovim PIN-om možeš otključati Bitwarden. Tvoj PIN će se resetirati ako se ikada potpuno odjaviš iz aplikacije." }, "pinRequired": { "message": "Potreban je PIN." @@ -2208,7 +2208,7 @@ "message": "Koristi ovu lozinku" }, "useThisPassphrase": { - "message": "Use this passphrase" + "message": "Koristi ovu fraznu lozinku" }, "useThisUsername": { "message": "Koristi ovo korisničko ime" @@ -2380,7 +2380,7 @@ "message": "Pravila privatnosti" }, "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { - "message": "Your new password cannot be the same as your current password." + "message": "Tvoja nova lozinka ne može biti ista kao tvoja trenutna lozinka." }, "hintEqualsPassword": { "message": "Podsjetnik za lozinku ne može biti isti kao lozinka." @@ -2488,10 +2488,10 @@ "message": "Organizacijsko pravilo onemogućuje uvoz stavki u tvoj osobni trezor." }, "restrictCardTypeImport": { - "message": "Cannot import card item types" + "message": "Ne mogu uvesti stavke vrste Platna kartica" }, "restrictCardTypeImportDesc": { - "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + "message": "Pravila jedne ili više organizacija onemogućuju uvoz stavke vrste Platna kartica u tvoj trezor." }, "domainsTitle": { "message": "Domene", @@ -2525,7 +2525,7 @@ "message": "Promijeni" }, "changePassword": { - "message": "Change password", + "message": "Promijeni lozinku", "description": "Change password button for browser at risk notification on login." }, "changeButtonTitle": { @@ -2538,7 +2538,7 @@ } }, "atRiskPassword": { - "message": "At-risk password" + "message": "Rizična lozinka" }, "atRiskPasswords": { "message": "Rizične lozinke" @@ -2575,7 +2575,7 @@ } }, "atRiskChangePrompt": { - "message": "Your password for this site is at-risk. $ORGANIZATION$ has requested that you change it.", + "message": "Tvoja lozinka za ovo mjesto je rizična. $ORGANIZATION$ zahtijeva da ju promijeniš.", "placeholders": { "organization": { "content": "$1", @@ -2585,7 +2585,7 @@ "description": "Notification body when a login triggers an at-risk password change request and the change password domain is known." }, "atRiskNavigatePrompt": { - "message": "$ORGANIZATION$ wants you to change this password because it is at-risk. Navigate to your account settings to change the password.", + "message": "$ORGANIZATION$ želi da promjeniš ovu lozinku jer je rizična. Promijeni lozinku u postavkama računa.", "placeholders": { "organization": { "content": "$1", @@ -2623,14 +2623,14 @@ "description": "Description of the review at-risk login slide on the at-risk password page carousel" }, "reviewAtRiskLoginSlideImgAltPeriod": { - "message": "Illustration of a list of logins that are at-risk." + "message": "Ilustracija liste rizičnih prijava." }, "generatePasswordSlideDesc": { "message": "Brzo generiraj jake, jedinstvene lozinke koristeći Bitwarden dijalog auto-ispune direktno na stranici.", "description": "Description of the generate password slide on the at-risk password page carousel" }, "generatePasswordSlideImgAltPeriod": { - "message": "Illustration of the Bitwarden autofill menu displaying a generated password." + "message": "Ilustracija Bitwarden dijaloga auto-ispune s prikazom generirane lozinke." }, "updateInBitwarden": { "message": "Ažuriraj u Bitwardenu" @@ -2640,7 +2640,7 @@ "description": "Description of the update in Bitwarden slide on the at-risk password page carousel" }, "updateInBitwardenSlideImgAltPeriod": { - "message": "Illustration of a Bitwarden’s notification prompting the user to update the login." + "message": "Ilustracija Bitwarden upita za ažuriranje prijave." }, "turnOnAutofill": { "message": "Uključi auto-ispunu" @@ -2714,7 +2714,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "maxAccessCountReached": { - "message": "Max access count reached", + "message": "Dostignut najveći broj pristupanja", "description": "This text will be displayed after a Send has been accessed the maximum amount of times." }, "hideTextByDefault": { @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Moraš ovjeriti svoju e-poštu u mrežnom trezoru za koritšenje ove značajke." }, + "masterPasswordSuccessfullySet": { + "message": "Glavna lozinka uspješno postavljena" + }, "updatedMasterPassword": { "message": "Glavna lozinka ažurirana" }, @@ -3058,13 +3061,13 @@ "message": "Nije nađen jedinstveni identifikator." }, "removeMasterPasswordForOrganizationUserKeyConnector": { - "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + "message": "Glavna lozinka više nije obavezna za članove sljedeće organizacije. Provjeri prikazanu domenu sa svojim administratorom." }, "organizationName": { - "message": "Organization name" + "message": "Naziv Organizacije" }, "keyConnectorDomain": { - "message": "Key Connector domain" + "message": "Domena konektora ključa" }, "leaveOrganization": { "message": "Napusti organizaciju" @@ -3627,35 +3630,35 @@ "message": "Uređaj pouzdan" }, "trustOrganization": { - "message": "Trust organization" + "message": "Vjeruj organizaciji" }, "trust": { - "message": "Trust" + "message": "Vjeruj" }, "doNotTrust": { - "message": "Do not trust" + "message": "Nemoj vjerovati" }, "organizationNotTrusted": { - "message": "Organization is not trusted" + "message": "Organizacija nije pouzdana" }, "emergencyAccessTrustWarning": { - "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + "message": "Radi sigurnosti tvojeg računa, potvrdi samo ako je ovom korisniku odobren pristup u nuždi i ako se otisak prsta podudara s onim prikazanim u računu" }, "orgTrustWarning": { - "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + "message": "Radi sigurnosti tvojeg računa, nastavi samo ako si član ove organizacije, imaš omogućen oporavak računa i otisak prsta prikazan u nastavku odgovara otisku prsta organizacije." }, "orgTrustWarning1": { - "message": "This organization has an Enterprise policy that will enroll you in account recovery. Enrollment will allow organization administrators to change your password. Only proceed if you recognize this organization and the fingerprint phrase displayed below matches the organization's fingerprint." + "message": "Ova organizacija ima poslovno pravilo koje će te prijaviti za oporavak računa. Prijava će omogućiti administratorima organizacije da promijene tvoju lozinku. Nastavi samo ako prepoznaješ ovu organizaciju i ako se fraza otiska prsta prikazana u nastavku podudara s otiskom prsta organizacije." }, "trustUser": { - "message": "Trust user" + "message": "Vjeruj korisniku" }, "sendsTitleNoItems": { - "message": "Send sensitive information safely", + "message": "Sigurno pošalji osjetljive podatke", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsBodyNoItems": { - "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", + "message": "Sigurno dijeli datoteke i podatke s bilo kime, na bilo kojoj platformi. Tvoji podaci ostaju kriptirani uz ograničenje izloženosti.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { @@ -4244,6 +4247,26 @@ "message": "Uobičajeni oblici", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Nastavi na postavke preglednika?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" @@ -4390,7 +4413,7 @@ } }, "viewItemTitleWithField": { - "message": "View item - $ITEMNAME$ - $FIELD$", + "message": "Pogledaj stavku - $ITEMNAME$ - $FIELD$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -4414,7 +4437,7 @@ } }, "autofillTitleWithField": { - "message": "Autofill - $ITEMNAME$ - $FIELD$", + "message": "Auto-ispuna - $ITEMNAME$ - $FIELD$", "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { @@ -4575,31 +4598,31 @@ } }, "downloadBitwarden": { - "message": "Download Bitwarden" + "message": "Preuzmi Bitwarden" }, "downloadBitwardenOnAllDevices": { - "message": "Download Bitwarden on all devices" + "message": "Preuzmi Bitwarden na svim uređajima" }, "getTheMobileApp": { - "message": "Get the mobile app" + "message": "Nabavi mobilnu aplikaciju" }, "getTheMobileAppDesc": { - "message": "Access your passwords on the go with the Bitwarden mobile app." + "message": "Pristupi svojim lozinkama bilo gdje s Bitwarden mobilnom aplikacijom." }, "getTheDesktopApp": { - "message": "Get the desktop app" + "message": "Nabavi aplikaciju za stolna računala" }, "getTheDesktopAppDesc": { - "message": "Access your vault without a browser, then set up unlock with biometrics to expedite unlocking in both the desktop app and browser extension." + "message": "Pristupi svojem trezoru bez preglednika, a zatim postavi otključavanje biometrijom za brže otključavanje aplikacije za stolna računala i prošitenja preglednika." }, "downloadFromBitwardenNow": { - "message": "Download from bitwarden.com now" + "message": "Odmah preusmi s bitwarden.com" }, "getItOnGooglePlay": { - "message": "Get it on Google Play" + "message": "Nabavi u Google Play" }, "downloadOnTheAppStore": { - "message": "Download on the App Store" + "message": "Nabavi u App Store" }, "permanentlyDeleteAttachmentConfirmation": { "message": "Sigurno želiš trajno izbrisati ovaj privitak?" @@ -5063,16 +5086,16 @@ "message": "Biometrijsko otključavanje trenutno nije dostupno iz nepoznatog razloga." }, "unlockVault": { - "message": "Unlock your vault in seconds" + "message": "Otključaj svoj trezor u trenu" }, "unlockVaultDesc": { - "message": "You can customize your unlock and timeout settings to more quickly access your vault." + "message": "Možeš prilagoditi postavke otključavanja i vremena isteka za brže pristupanje svojem trezoru." }, "unlockPinSet": { - "message": "Unlock PIN set" + "message": "PIN za otključavanje podešen" }, "unlockWithBiometricSet": { - "message": "Unlock with biometrics set" + "message": "Otključavanje biometrijom postavljeno" }, "authenticating": { "message": "Autentifikacija" @@ -5086,7 +5109,7 @@ "description": "Notification message for when a password has been regenerated" }, "saveToBitwarden": { - "message": "Save to Bitwarden", + "message": "Spremi u Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { @@ -5291,133 +5314,133 @@ "message": "Promijeni rizičnu lozinku" }, "settingsVaultOptions": { - "message": "Vault options" + "message": "Mogućnosti trezora" }, "emptyVaultDescription": { - "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + "message": "Trezor štiti više od lozinki. Sigurno spremi prijave, identitete, kartice i bilješke." }, "introCarouselLabel": { - "message": "Welcome to Bitwarden" + "message": "Dobrodošli u Bitwarden" }, "securityPrioritized": { - "message": "Security, prioritized" + "message": "Sigurnost je imperativ" }, "securityPrioritizedBody": { - "message": "Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you." + "message": "Spremi prijave, kartice i identitete u svoj sigurni trezor. Bitwarden koristi end-to-end enkripciju bez znanja kako bi zaštitio ono što ti je važno." }, "quickLogin": { - "message": "Quick and easy login" + "message": "Brza i jednostavna prijava" }, "quickLoginBody": { - "message": "Set up biometric unlock and autofill to log into your accounts without typing a single letter." + "message": "Uključi biometrijsko otključavanje i auto-ispunu za prijavu bez utipkavanja ijednog slova." }, "secureUser": { - "message": "Level up your logins" + "message": "Podigni svoje prijave na višu razinu" }, "secureUserBody": { - "message": "Use the generator to create and save strong, unique passwords for all your accounts." + "message": "Koristi generator za stvaranje i spremanje jakih, jedinstvenih lozinki za sve svoje račune." }, "secureDevices": { - "message": "Your data, when and where you need it" + "message": "Tvoji podaci kada god i gdje god su ti potrebni" }, "secureDevicesBody": { - "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + "message": "Spremi neograničen broj lozinki na neograničenom broju uređaja s Bitwarden mobilnom aplikacijom, proširenjem za preglednik i aplikacijom za stolna računala." }, "nudgeBadgeAria": { - "message": "1 notification" + "message": "1 obavijest" }, "emptyVaultNudgeTitle": { - "message": "Import existing passwords" + "message": "Uvezi postojeće lozinke" }, "emptyVaultNudgeBody": { - "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + "message": "Koristi uvoz za brzi prijenos prijava u Bitwarden bez ručnog dodavanja." }, "emptyVaultNudgeButton": { - "message": "Import now" + "message": "Uvezi odmah" }, "hasItemsVaultNudgeTitle": { - "message": "Welcome to your vault!" + "message": "Dobrodošli u svoj trezor!" }, "hasItemsVaultNudgeBodyOne": { - "message": "Autofill items for the current page" + "message": "Auto-ispuni stavke za trenutnu stranicu" }, "hasItemsVaultNudgeBodyTwo": { - "message": "Favorite items for easy access" + "message": "Favoriti za brzi pristup" }, "hasItemsVaultNudgeBodyThree": { - "message": "Search your vault for something else" + "message": "Nađi nešto drugo u svom trezoru" }, "newLoginNudgeTitle": { - "message": "Save time with autofill" + "message": "Uštedi vrijeme s auto-ispunom" }, "newLoginNudgeBodyOne": { - "message": "Include a", + "message": "Uključi", "description": "This is in multiple parts to allow for bold text in the middle of the sentence.", "example": "Include a Website so this login appears as an autofill suggestion." }, "newLoginNudgeBodyBold": { - "message": "Website", + "message": "web stranicu", "description": "This is in multiple parts to allow for bold text in the middle of the sentence.", "example": "Include a Website so this login appears as an autofill suggestion." }, "newLoginNudgeBodyTwo": { - "message": "so this login appears as an autofill suggestion.", + "message": "kako bi ova prijava bila predložena u auto-ispuni.", "description": "This is in multiple parts to allow for bold text in the middle of the sentence.", "example": "Include a Website so this login appears as an autofill suggestion." }, "newCardNudgeTitle": { - "message": "Seamless online checkout" + "message": "Jednostavna online kupnja" }, "newCardNudgeBody": { - "message": "With cards, easily autofill payment forms securely and accurately." + "message": "S karticama jednostavno i sigurno automatski ispunjavaš obrasce za plaćanje." }, "newIdentityNudgeTitle": { - "message": "Simplify creating accounts" + "message": "Pojednostavi stvaranje računa" }, "newIdentityNudgeBody": { - "message": "With identities, quickly autofill long registration or contact forms." + "message": "S identitetima brzo automatski ispunjavaš duge registracijske i kontaktne obrasce." }, "newNoteNudgeTitle": { - "message": "Keep your sensitive data safe" + "message": "Čuvaj svoje osjetljive podatke na sigurnom" }, "newNoteNudgeBody": { - "message": "With notes, securely store sensitive data like banking or insurance details." + "message": "S bilješkama sigurno pohrani svoje osjetljive podatke poput podataka o banci ili osiguranju." }, "newSshNudgeTitle": { - "message": "Developer-friendly SSH access" + "message": "SSH pristup prilagođen programerima" }, "newSshNudgeBodyOne": { - "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication.", + "message": "Pohrani svoje ključeve i poveži se sa SSH agentom za brzu, šifriranu autentifikaciju.", "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "newSshNudgeBodyTwo": { - "message": "Learn more about SSH agent", + "message": "Saznaj više o SSH agentu", "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "generatorNudgeTitle": { - "message": "Quickly create passwords" + "message": "Brzo stvori lozinke" }, "generatorNudgeBodyOne": { - "message": "Easily create strong and unique passwords by clicking on", + "message": "Jednostavno stvori jake i jedinstvene lozinke odabirom tipke", "description": "Two part message", "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." }, "generatorNudgeBodyTwo": { - "message": "to help you keep your logins secure.", + "message": "kako bi tvoje prijave ostale sigurne.", "description": "Two part message", "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." }, "generatorNudgeBodyAria": { - "message": "Easily create strong and unique passwords by clicking on the Generate password button to help you keep your logins secure.", + "message": "Jednostavno stvori jake i sigurne lozinke odabirom tipke Generiraj lozinku kako bi tvoje prijave ostale sigurne.", "description": "Aria label for the body content of the generator nudge" }, "noPermissionsViewPage": { - "message": "You do not have permissions to view this page. Try logging in with a different account." + "message": "Nemaš dozvolu za pregled ove stranice. Pokušaj se prijaviti s drugim računom." }, "wasmNotSupported": { - "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", + "message": "WebAssembly ili nije podržan ili nije omogućen u tvom pregledniku. WebAssembly je potreban za korištenje Bitwarden aplikacije.", "description": "'WebAssembly' is a technical term and should not be translated." } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index c85de0ab542..b2800be9c81 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "A funkció használatához igazolni kell email címet. Az email cím a webtárban ellenőrizhető." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "A mesterjelszó frissítésre került." }, @@ -4244,6 +4247,26 @@ "message": "Általános formátumok", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 125cd7ceeab..ccf35bf05d0 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Anda harus memverifikasi email Anda untuk menggunakan fitur ini. Anda dapat memverifikasi email Anda di brankas web." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Kata Sandi Utama Telah Diperbarui" }, @@ -4244,6 +4247,26 @@ "message": "Format umum", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Lanjutkan ke pengaturan peramban?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index f1c2cd09ca2..24c18b1ea7b 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Devi verificare la tua email per usare questa funzionalità. Puoi verificare la tua email nella cassaforte web." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Password principale aggiornata" }, @@ -4244,6 +4247,26 @@ "message": "Formati comuni", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continua sulle impostazioni del browser?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index fd256961ba6..1a19ffe1ae1 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "この機能を使用するにはメールアドレスを確認する必要があります。ウェブ保管庫でメールアドレスを確認できます。" }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "マスターパスワードを更新しました" }, @@ -4244,6 +4247,26 @@ "message": "一般的な形式", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "ブラウザの設定に進みますか?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 68a6a877e50..bb2977f31eb 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Updated master password" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index b6a8d1834b4..45dc3215966 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Updated master password" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 987a2ce79cb..f34e98d3643 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "ಈ ವೈಶಿಷ್ಟ್ಯವನ್ನು ಬಳಸಲು ನಿಮ್ಮ ಇಮೇಲ್ ಅನ್ನು ನೀವು ಪರಿಶೀಲಿಸಬೇಕು. ವೆಬ್ ವಾಲ್ಟ್ನಲ್ಲಿ ನಿಮ್ಮ ಇಮೇಲ್ ಅನ್ನು ನೀವು ಪರಿಶೀಲಿಸಬಹುದು." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Updated master password" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 41c01431ac2..abd7ff86247 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "이 기능을 사용하려면 이메일 인증이 필요합니다. 웹 보관함에서 이메일을 인증할 수 있습니다." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "마스터 비밀번호 변경됨" }, @@ -4244,6 +4247,26 @@ "message": "일반적인 형식", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "브라우저 설정으로 이동하시겠습니까?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 3c2751d5fbe..b800a93241f 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Turite patvirtinti savo el. paštą, kad galėtumėte naudotis šia funkcija. Savo el. pašto adresą galite patvirtinti žiniatinklio saugykloje." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Naujasis pagrindinis slaptažodis" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 7108fe15a93..ad86e857818 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Ir nepieciešams apliecināt savu e-pasta adresi, lai būtu iespējams izmantot šo iespēju. To var izdarīt tīmekļa glabātavā." }, + "masterPasswordSuccessfullySet": { + "message": "Galvenā parole sekmīgi iestatīta" + }, "updatedMasterPassword": { "message": "Galvenā parole atjaunināta" }, @@ -4244,6 +4247,26 @@ "message": "Izplatīti veidoli", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Pāriet uz pārlūka iestatījumiem?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 7708ea4b940..df0f2b4ebdd 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Updated master password" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 36fdc74f521..1655daf6b37 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Updated master password" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index b6a8d1834b4..45dc3215966 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Updated master password" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 747e37aadd6..419f995d83c 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Du må bekrefte e-posten din for å bruke denne funksjonen. Du kan bekrefte e-postadressen din i netthvelvet." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Oppdaterte hovedpassordet" }, @@ -4244,6 +4247,26 @@ "message": "Vanlige formater", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Vil du fortsette til nettleserinnstillingene?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index b6a8d1834b4..45dc3215966 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Updated master password" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index af63059ebd3..6c1b03322ad 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Je moet je e-mailadres verifiëren om deze functie te gebruiken. Je kunt je e-mailadres verifiëren in de kluis." }, + "masterPasswordSuccessfullySet": { + "message": "Hoofdwachtwoord succesvol ingesteld" + }, "updatedMasterPassword": { "message": "Hoofdwachtwoord bijgewerkt" }, @@ -4244,6 +4247,26 @@ "message": "Veelvoorkomende formaten", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI-matche-detectie is hoe Bitwarden invulsuggesties herkent.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Reguliere expressie\" is een geavanceerde optie met een verhoogd risico op het blootstellen van inloggegevens.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Begint met\" is een geavanceerde optie met een verhoogd risico op het blootstellen van inloggegevens.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "Lees meer over overeenkomstdetectie", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Geavanceerde opties", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Doorgaan naar browserinstellingen?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index b6a8d1834b4..45dc3215966 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Updated master password" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index b6a8d1834b4..45dc3215966 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Updated master password" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index dba3a21d160..ae82f480964 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -3,10 +3,10 @@ "message": "Bitwarden" }, "appLogoLabel": { - "message": "Bitwarden logo" + "message": "Logo Bitwarden" }, "extName": { - "message": "Menedżer Haseł Bitwarden", + "message": "Menedżer haseł Bitwarden", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { @@ -23,10 +23,10 @@ "message": "Utwórz konto" }, "newToBitwarden": { - "message": "Nowy użytkownik Bitwarden?" + "message": "Nowy w Bitwarden?" }, "logInWithPasskey": { - "message": "Zaloguj się używając klucza dostępu" + "message": "Logowaniem kluczem dostępu" }, "useSingleSignOn": { "message": "Użyj jednokrotnego logowania" @@ -65,7 +65,7 @@ "message": "Podpowiedź do hasła głównego może pomóc Ci przypomnieć hasło, jeśli je zapomnisz." }, "masterPassHintText": { - "message": "Jeśli zapomnisz hasła, podpowiedź hasła może zostać wysłana na Twój adres e-mail. $CURRENT$ z $MAXIMUM$ znaków.", + "message": "Podpowiedź do hasła zostanie wysłana na adres e-mail, jeśli je zapomnisz. Liczba znaków: $CURRENT$ / $MAXIMUM$.", "placeholders": { "current": { "content": "$1", @@ -132,7 +132,7 @@ "message": "Kopiuj hasło" }, "copyPassphrase": { - "message": "Skopiuj hasło wyrazowe" + "message": "Kopiuj hasło wyrazowe" }, "copyNote": { "message": "Kopiuj notatkę" @@ -159,19 +159,19 @@ "message": "Kopiuj numer PESEL" }, "copyPassportNumber": { - "message": "Skopiuj numer paszportu" + "message": "Kopiuj numer paszportu" }, "copyLicenseNumber": { - "message": "Kopiuj numer licencji" + "message": "Kopiuj numer prawa jazdy" }, "copyPrivateKey": { - "message": "Skopiuj klucz prywatny" + "message": "Kopiuj klucz prywatny" }, "copyPublicKey": { - "message": "Skopiuj klucz publiczny" + "message": "Kopiuj klucz publiczny" }, "copyFingerprint": { - "message": "Skopiuj odcisk palca" + "message": "Kopiuj odcisk klucza" }, "copyCustomField": { "message": "Kopiuj $FIELD$", @@ -193,26 +193,26 @@ "description": "Copy to clipboard" }, "fill": { - "message": "Wypełnij", + "message": "Uzupełnij", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { "message": "Autouzupełnianie" }, "autoFillLogin": { - "message": "Autouzupełnianie logowania" + "message": "Uzupełnij dane logowania" }, "autoFillCard": { - "message": "Autouzupełnianie karty" + "message": "Uzupełnij kartę" }, "autoFillIdentity": { - "message": "Autouzupełnianie tożsamości" + "message": "Uzupełnij tożsamość" }, "fillVerificationCode": { - "message": "Wypełnij kod weryfikacyjny" + "message": "Uzupełnij kod weryfikacyjny" }, "fillVerificationCodeAria": { - "message": "Wypełnij kod weryfikacyjny", + "message": "Uzupełnij kod weryfikacyjny", "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { @@ -258,13 +258,13 @@ "message": "Adres e-mail konta" }, "requestHint": { - "message": "Poproś o podpowiedź" + "message": "Uzyskaj podpowiedź" }, "requestPasswordHint": { - "message": "Poproś o podpowiedź do hasła" + "message": "Uzyskaj podpowiedź do hasła" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { - "message": "Wprowadź adres e-mail swojego konta, a podpowiedź hasła zostanie wysłana do Ciebie" + "message": "Wpisz adres e-mail konta. Podpowiedź do hasła zostanie wysłana na adres e-mail" }, "getMasterPasswordHint": { "message": "Uzyskaj podpowiedź do hasła głównego" @@ -291,19 +291,19 @@ "message": "Zmień hasło główne" }, "continueToWebApp": { - "message": "Kontynuować do aplikacji internetowej?" + "message": "Przejść do aplikacji internetowej?" }, "continueToWebAppDesc": { "message": "Odkryj więcej funkcji swojego konta Bitwarden w aplikacji internetowej." }, "continueToHelpCenter": { - "message": "Kontynuować do centrum pomocy?" + "message": "Przejść do centrum pomocy?" }, "continueToHelpCenterDesc": { "message": "Dowiedz się więcej o tym, jak korzystać z centrum pomocy Bitwarden." }, "continueToBrowserExtensionStore": { - "message": "Kontynuować do sklepu z rozszerzeniami przeglądarki?" + "message": "Przejść do sklepu z rozszerzeniami przeglądarki?" }, "continueToBrowserExtensionStoreDesc": { "message": "Pomóż innym dowiedzieć się, czy Bitwarden jest dla nich odpowiedni. Odwiedź swój sklep z rozszerzeniami do przeglądarki i zostaw ocenę." @@ -316,7 +316,7 @@ "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." }, "yourAccountsFingerprint": { - "message": "Unikalny identyfikator Twojego konta", + "message": "Twój unikalny identyfikator konta", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." }, "twoStepLogin": { @@ -335,7 +335,7 @@ "message": "Więcej od Bitwarden" }, "continueToBitwardenDotCom": { - "message": "Kontynuować do bitwarden.com?" + "message": "Przejść do bitwarden.com?" }, "bitwardenForBusiness": { "message": "Bitwarden dla biznesu" @@ -407,7 +407,7 @@ "message": "Twórz foldery, aby zorganizować elementy swojego sejfu" }, "deleteFolderPermanently": { - "message": "Czy na pewno chcesz trwale usunąć ten folder?" + "message": "Czy na pewno chcesz usunąć trwale folder?" }, "deleteFolder": { "message": "Usuń folder" @@ -474,7 +474,7 @@ "message": "Nazwa użytkownika została wygenerowana" }, "emailGenerated": { - "message": "E-mail został wygenerowany" + "message": "Adres e-mail został wygenerowany" }, "regeneratePassword": { "message": "Wygeneruj ponownie hasło" @@ -490,7 +490,7 @@ "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Uwzględnij wielkie litery", + "message": "Wielkie litery", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -498,7 +498,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Uwzględnij małe litery", + "message": "Małe litery", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -506,7 +506,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Uwzględnij cyfry", + "message": "Cyfry", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -514,7 +514,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Uwzględnij znaki specjalne", + "message": "Znaki specjalne", "description": "Full description for the password generator special characters checkbox" }, "numWords": { @@ -531,10 +531,10 @@ "message": "Uwzględnij cyfry" }, "minNumbers": { - "message": "Minimalna liczba cyfr" + "message": "Min. liczba cyfr" }, "minSpecial": { - "message": "Minimalna liczba znaków specjalnych" + "message": "Min. liczba znaków specjalnych" }, "avoidAmbiguous": { "message": "Unikaj niejednoznacznych znaków", @@ -551,10 +551,10 @@ "message": "Edytuj" }, "view": { - "message": "Zobacz" + "message": "Pokaż" }, "noItemsInList": { - "message": "Brak elementów." + "message": "Brak elementów do wyświetlenia." }, "itemInformation": { "message": "Informacje o elemencie" @@ -566,22 +566,22 @@ "message": "Hasło" }, "totp": { - "message": "Sekret uwierzytelniania" + "message": "Klucz uwierzytelniający" }, "passphrase": { "message": "Hasło wyrazowe" }, "favorite": { - "message": "Ulubione" + "message": "Dodaj do ulubionych" }, "unfavorite": { "message": "Usuń z ulubionych" }, "itemAddedToFavorites": { - "message": "Element dodany do ulubionych" + "message": "Element został dodany do ulubionych" }, "itemRemovedFromFavorites": { - "message": "Element usunięty z ulubionych" + "message": "Element został usunięty z ulubionych" }, "notes": { "message": "Notatki" @@ -602,16 +602,16 @@ "message": "Usuń element" }, "viewItem": { - "message": "Zobacz element" + "message": "Pokaż element" }, "launch": { "message": "Uruchom" }, "launchWebsite": { - "message": "Otwórz stronę" + "message": "Uruchom stronę internetową" }, "launchWebsiteName": { - "message": "Otwórz stronę internetową $ITEMNAME$", + "message": "Uruchom stronę internetową $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -620,10 +620,10 @@ } }, "website": { - "message": "Strona" + "message": "Strona internetowa" }, "toggleVisibility": { - "message": "Pokaż / Ukryj" + "message": "Pokaż / ukryj" }, "manage": { "message": "Zarządzaj" @@ -641,7 +641,7 @@ "message": "Ustaw metodę odblokowania w Ustawieniach" }, "sessionTimeoutHeader": { - "message": "Limit czasu sesji" + "message": "Blokada aplikacji" }, "vaultTimeoutHeader": { "message": "Blokowanie sejfu" @@ -656,7 +656,7 @@ "message": "Przeglądarka nie obsługuje łatwego kopiowania schowka. Skopiuj element ręcznie." }, "verifyYourIdentity": { - "message": "Potwierdź swoją tożsamość" + "message": "Zweryfikuj tożsamość" }, "weDontRecognizeThisDevice": { "message": "Nie rozpoznajemy tego urządzenia. Wpisz kod wysłany na Twój e-mail, aby zweryfikować tożsamość." @@ -668,10 +668,10 @@ "message": "Sejf jest zablokowany. Zweryfikuj swoją tożsamość, aby kontynuować." }, "yourVaultIsLockedV2": { - "message": "Twój sejf jest zablokowany" + "message": "Sejf jest zablokowany" }, "yourAccountIsLocked": { - "message": "Twoje konto jest zablokowane" + "message": "Konto jest zablokowane" }, "or": { "message": "lub" @@ -705,7 +705,7 @@ "message": "Zablokuj" }, "lockAll": { - "message": "Zablokuj wszystkie" + "message": "Zablokuj wszystko" }, "immediately": { "message": "Natychmiast" @@ -780,7 +780,7 @@ "message": "Wymagane jest ponowne wpisanie hasła głównego." }, "masterPasswordMinlength": { - "message": "Hasło główne musi zawierać co najmniej $VALUE$ znaki(-ów).", + "message": "Hasło główne musi składać się z co najmniej $VALUE$ znaków.", "description": "The Master Password must be at least a specific number of characters long.", "placeholders": { "value": { @@ -796,10 +796,10 @@ "message": "Konto zostało utworzone! Teraz możesz się zalogować." }, "newAccountCreated2": { - "message": "Twoje nowe konto zostało utworzone!" + "message": "Nowe konto zostało utworzone!" }, "youHaveBeenLoggedIn": { - "message": "Zalogowano Cię!" + "message": "Zalogowano!" }, "youSuccessfullyLoggedIn": { "message": "Zalogowałeś się pomyślnie" @@ -808,7 +808,7 @@ "message": "Możesz zamknąć to okno" }, "masterPassSent": { - "message": "Wysłaliśmy Tobie wiadomość e-mail z podpowiedzią do hasła głównego." + "message": "Wysłaliśmy wiadomość z podpowiedzią do hasła głównego." }, "verificationCodeRequired": { "message": "Kod weryfikacyjny jest wymagany." @@ -839,7 +839,7 @@ "message": "Klucz uwierzytelniający został dodany" }, "totpCapture": { - "message": "Zeskanuj kod QR z bieżącej strony" + "message": "Zeskanuj kod QR z obecnej strony" }, "totpHelperTitle": { "message": "Spraw, aby dwuetapowa weryfikacja była bezproblemowa" @@ -890,7 +890,7 @@ "message": "Wykonaj poniższe kroki, aby zakończyć logowanie za pomocą klucza bezpieczeństwa." }, "restartRegistration": { - "message": "Zrestartuj rejestrację" + "message": "Zacznij rejestrację od początku" }, "expiredLink": { "message": "Link wygasł" @@ -929,13 +929,13 @@ "message": "Spraw, aby Twoje konto było bezpieczniejsze poprzez skonfigurowanie dwustopniowego logowania w aplikacji internetowej Bitwarden." }, "twoStepLoginConfirmationTitle": { - "message": "Kontynuować do aplikacji internetowej?" + "message": "Przejść do aplikacji internetowej?" }, "editedFolder": { "message": "Folder został zapisany" }, "deleteFolderConfirmation": { - "message": "Czy na pewno chcesz usunąć ten folder?" + "message": "Czy na pewno chcesz usunąć folder?" }, "deletedFolder": { "message": "Folder został usunięty" @@ -982,7 +982,7 @@ "message": "Element został zapisany" }, "deleteItemConfirmation": { - "message": "Czy na pewno chcesz to usunąć?" + "message": "Czy na pewno chcesz usunąć?" }, "deletedItem": { "message": "Element został przeniesiony do kosza" @@ -1013,7 +1013,7 @@ "description": "This is the folder for uncategorized items" }, "enableAddLoginNotification": { - "message": "Poproś o dodanie danych logowania" + "message": "Proponuj zapisywanie danych logowania" }, "vaultSaveOptionsTitle": { "message": "Opcje zapisywania w sejfie" @@ -1025,7 +1025,7 @@ "message": "Poproś o dodanie elementu, jeśli nie zostanie znaleziony w Twoim sejfie. Dotyczy wszystkich zalogowanych kont." }, "showCardsInVaultViewV2": { - "message": "Zawsze pokazuj karty jako sugestie autouzupełniania w widoku sejfu" + "message": "Pokazuj zawsze karty w sugestiach autouzupełniania" }, "showCardsCurrentTab": { "message": "Pokaż karty na stronie głównej" @@ -1034,7 +1034,7 @@ "message": "Pokaż elementy karty na stronie głównej, aby ułatwić autouzupełnianie." }, "showIdentitiesInVaultViewV2": { - "message": "Zawsze pokazuj tożsamości jako sugestie autouzupełniania w widoku sejfu" + "message": "Pokazuj zawsze tożsamości w sugestiach autouzupełniania" }, "showIdentitiesCurrentTab": { "message": "Pokaż tożsamości na stronie głównej" @@ -1113,7 +1113,7 @@ } }, "saveAsNewLoginAction": { - "message": "Zapisz jako nowy login", + "message": "Zapisz jako nowy dane logowania", "description": "Button text for saving login details as a new entry." }, "updateLoginAction": { @@ -1121,7 +1121,7 @@ "description": "Button text for updating an existing login entry." }, "unlockToSave": { - "message": "Odblokuj, aby zapisać ten login", + "message": "Odblokuj, aby zapisać dane logowania", "description": "User prompt to take action in order to save the login they just entered." }, "saveLogin": { @@ -1129,11 +1129,11 @@ "description": "Prompt asking the user if they want to save their login details." }, "updateLogin": { - "message": "Zaktualizować istniejące dane logowania?", + "message": "Zaktualizuj obecne dane logowania", "description": "Prompt asking the user if they want to update an existing login entry." }, "loginSaveSuccess": { - "message": "Dane logowania zapisane", + "message": "Dane logowania zostały zapisane", "description": "Message displayed when login details are successfully saved." }, "loginUpdateSuccess": { @@ -1166,7 +1166,7 @@ "description": "Message prompting user to undertake completion of another security task." }, "saveFailure": { - "message": "Błąd podczas zapisywania", + "message": "Wystąpił błąd podczas zapisywania", "description": "Error message shown when the system fails to save login details." }, "saveFailureDetails": { @@ -1174,7 +1174,7 @@ "description": "Detailed error message shown when saving login details fails." }, "enableChangedPasswordNotification": { - "message": "Poproś o aktualizację istniejących danych logowania" + "message": "Proponuj aktualizuję obecnych danych logowania" }, "changedPasswordNotificationDesc": { "message": "Poproś o aktualizację hasła danych logowania po wykryciu zmiany w witrynie." @@ -1183,10 +1183,10 @@ "message": "Poproś o aktualizację hasła, gdy zmiana zostanie wykryta na stronie. Dotyczy wszystkich zalogowanych kont." }, "enableUsePasskeys": { - "message": "Pytaj o zapisywanie i używanie kluczy dostępu" + "message": "Proponuj zapisywanie i używanie kluczy dostępu" }, "usePasskeysDesc": { - "message": "Pytaj o zapisywanie nowych kluczy dostępu albo danych logowania z kluczy w Twoim sejfie. Dotyczy wszystkich zalogowanych kont." + "message": "Pytaj o zapisywanie nowych kluczy dostępu i używanie obecnych. Dotyczy wszystkich zalogowanych kont." }, "notificationChangeDesc": { "message": "Czy chcesz zaktualizować to hasło w Bitwarden?" @@ -1204,7 +1204,7 @@ "message": "Dodatkowe opcje" }, "enableContextMenuItem": { - "message": "Pokaż opcje menu kontekstowego" + "message": "Pokaż opcje w menu kontekstowym" }, "contextMenuItemDesc": { "message": "Użyj drugiego kliknięcia, aby uzyskać dostęp do generowania haseł i pasujących danych logowania do witryny." @@ -1249,7 +1249,7 @@ "message": "Plik będzie chroniony hasłem, które będzie wymagane do odszyfrowania pliku." }, "filePassword": { - "message": "Hasło do pliku" + "message": "Hasło pliku" }, "exportPasswordDescription": { "message": "Hasło będzie używane do eksportowania i importowania pliku" @@ -1267,10 +1267,10 @@ "message": "Konto ograniczone" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“Hasło pliku” i “Potwierdź hasło pliku“ nie pasują do siebie." + "message": "Hasła pliku nie pasują do siebie." }, "warning": { - "message": "UWAGA", + "message": "OSTRZEŻENIE", "description": "WARNING (should stay in capitalized letters if the language permits)" }, "warningCapitalized": { @@ -1296,13 +1296,13 @@ "message": "Udostępnione" }, "bitwardenForBusinessPageDesc": { - "message": "Bitwarden dla biznesu pozwala na udostępnianie zawartości sejfu innym osobom za pośrednictwem organizacji. Dowiedz się wiecej na bitwarden.com." + "message": "Bitwarden dla biznesu pozwala na udostępnianie zawartości sejfu innym użytkownikom za pośrednictwem organizacji. Dowiedz się więcej na stronie bitwarden.com." }, "moveToOrganization": { "message": "Przenieś do organizacji" }, "movedItemToOrg": { - "message": "Element $ITEMNAME$ został przeniesiony do organizacji $ORGNAME$", + "message": "Przeniesiono $ITEMNAME$ do $ORGNAME$", "placeholders": { "itemname": { "content": "$1", @@ -1315,7 +1315,7 @@ } }, "moveToOrgDesc": { - "message": "Wybierz organizację, do której chcesz przenieść ten element. Ta czynność spowoduje utratę własności elementu i przenosi te uprawnienia do organizacji." + "message": "Wybierz organizację, do której chcesz przenieść element. Przeniesienie spowoduje zmianę własności elementu na organizację." }, "learnMore": { "message": "Dowiedz się więcej" @@ -1423,7 +1423,7 @@ "message": "Uaktualnij do wersji Premium i otrzymaj:" }, "premiumPrice": { - "message": "Wszystko to jedynie za $PRICE$ /rok!", + "message": "Tylko $PRICE$ / rok!", "placeholders": { "price": { "content": "$1", @@ -1432,7 +1432,7 @@ } }, "premiumPriceV2": { - "message": "Wszystko tylko za $PRICE$ rocznie!", + "message": "Tylko $PRICE$ rocznie!", "placeholders": { "price": { "content": "$1", @@ -1453,7 +1453,7 @@ "message": "Poproś o dane biometryczne przy uruchomieniu" }, "premiumRequired": { - "message": "Konto Premium jest wymagane" + "message": "Konto premium jest wymagane" }, "premiumRequiredDesc": { "message": "Konto Premium jest wymagane, aby skorzystać z tej funkcji." @@ -1571,7 +1571,7 @@ "message": "Adres URL serwera" }, "selfHostBaseUrl": { - "message": "URL samodzielnie hostowanego serwera", + "message": "Adres URL hostowanego serwera", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -1615,19 +1615,19 @@ } }, "turnOffAutofill": { - "message": "Wyłącz autouzupełnienie" + "message": "Wyłącz autouzupełnianie" }, "showInlineMenuLabel": { "message": "Pokaż sugestie autouzupełniania na polach formularza" }, "showInlineMenuIdentitiesLabel": { - "message": "Pokazuj tożsamości jako sugestie" + "message": "Pokaż tożsamości w sugestiach" }, "showInlineMenuCardsLabel": { - "message": "Pokazuj karty jako sugestie" + "message": "Pokaż karty w sugestiach" }, "showInlineMenuOnIconSelectionLabel": { - "message": "Wyświetlaj sugestie, kiedy ikona jest zaznaczona" + "message": "Pokaż sugestie po kliknięciu ikony" }, "showInlineMenuOnFormFieldsDescAlt": { "message": "Dotyczy wszystkich zalogowanych kont." @@ -1651,16 +1651,16 @@ "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoadSectionTitle": { - "message": "Włącz autouzupełnianie po załadowaniu strony" + "message": "Autouzupełnianie po załadowaniu strony" }, "enableAutoFillOnPageLoad": { - "message": "Włącz autouzupełnianie po załadowaniu strony" + "message": "Uzupełniaj po załadowaniu strony" }, "enableAutoFillOnPageLoadDesc": { - "message": "Jeśli zostanie wykryty formularz logowania, automatycznie uzupełnij dane logowania po załadowaniu strony." + "message": "Dane logowania zostaną uzupełnione po wykryciu formularza logowania na stronie." }, "experimentalFeature": { - "message": "Zaatakowane lub niezaufane witryny internetowe mogą wykorzystać funkcję autouzupełniania podczas wczytywania strony, aby wyrządzić szkody." + "message": "Zaatakowane strony internetowe mogą wykorzystywać autouzupełnianie do przejęcia danych logowania." }, "learnMoreAboutAutofillOnPageLoadLinkText": { "message": "Dowiedz się więcej o ryzyku" @@ -1681,10 +1681,10 @@ "message": "Użyj domyślnego ustawienia" }, "autoFillOnPageLoadYes": { - "message": "Automatycznie uzupełniaj po załadowaniu strony" + "message": "Uzupełniaj po załadowaniu strony" }, "autoFillOnPageLoadNo": { - "message": "Nie uzupełniaj automatycznie po załadowaniu strony" + "message": "Nie uzupełniaj po załadowaniu strony" }, "commandOpenPopup": { "message": "Otwórz sejf w oknie" @@ -1702,7 +1702,7 @@ "message": "Autouzupełnianie korzysta z ostatnio używanej tożsamości na tej stronie" }, "commandGeneratePasswordDesc": { - "message": "Wygeneruj nowe losowe hasło i skopiuj je do schowka." + "message": "Wygeneruj nowe hasło i skopiuj je do schowka" }, "commandLockVaultDesc": { "message": "Zablokuj sejf" @@ -1729,7 +1729,7 @@ "message": "Tekst" }, "cfTypeHidden": { - "message": "Pole maskowane" + "message": "Ukryty tekst" }, "cfTypeBoolean": { "message": "Wartość logiczna" @@ -1749,10 +1749,10 @@ "message": "Kliknięcie poza okno, w celu sprawdzenia wiadomość z kodem weryfikacyjnym spowoduje, że zostanie ono zamknięte. Czy chcesz otworzyć nowe okno tak, aby się nie zamknęło?" }, "popupU2fCloseMessage": { - "message": "Ta przeglądarka nie może przetworzyć żądania U2F w tym oknie. Czy chcesz otworzyć nowe okno przeglądarki, aby zalogować się przy pomocy klucza U2F?" + "message": "Ta przeglądarka nie może przetworzyć żądania U2F w wyskakującym oknie. Czy chcesz otworzyć nowe okno przeglądarki, aby zalogować się przy pomocy klucza U2F?" }, "enableFavicon": { - "message": "Pokaż ikony witryn" + "message": "Pokaż ikony stron internetowych" }, "faviconDesc": { "message": "Pokaż rozpoznawalny obraz obok danych logowania." @@ -1833,7 +1833,7 @@ "message": "Pan" }, "mrs": { - "message": "Mrs" + "message": "Pani (Mrs)" }, "ms": { "message": "Pani" @@ -1857,7 +1857,7 @@ "message": "Imię i nazwisko" }, "identityName": { - "message": "Nazwa profilu" + "message": "Nowa tożsamość" }, "company": { "message": "Firma" @@ -1875,7 +1875,7 @@ "message": "Adres e-mail" }, "phone": { - "message": "Telefon" + "message": "Numer telefonu" }, "address": { "message": "Adres" @@ -1911,7 +1911,7 @@ "message": "Dane logowania" }, "typeSecureNote": { - "message": "Bezpieczna notatka" + "message": "Notatka" }, "typeCard": { "message": "Karta" @@ -1944,7 +1944,7 @@ } }, "viewItemHeader": { - "message": "Zobacz $TYPE$", + "message": "Pokaż $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1965,7 +1965,7 @@ "message": "Jeśli zatwierdzisz, wszystkie wygenerowane hasła zostaną usunięte z historii generatora. Czy chcesz kontynuować mimo to?" }, "back": { - "message": "Powrót" + "message": "Wstecz" }, "collections": { "message": "Kolekcje" @@ -1998,7 +1998,7 @@ "message": "Dane logowania" }, "secureNotes": { - "message": "Bezpieczne notatki" + "message": "Notatki" }, "sshKeys": { "message": "Klucze SSH" @@ -2011,7 +2011,7 @@ "message": "Sprawdź, czy hasło zostało ujawnione." }, "passwordExposed": { - "message": "To hasło znajduje się w $VALUE$ wykradzionej(ych) bazie(ach) danych. Należy je zmienić.", + "message": "Hasło zostało ujawnione $VALUE$ raz(y) w wyciekach danych. Zmień je.", "placeholders": { "value": { "content": "$1", @@ -2020,14 +2020,14 @@ } }, "passwordSafe": { - "message": "To hasło nie znajduje się w żadnej znanej wykradzionej bazie danych. Powinno być bezpieczne." + "message": "Hasło nie znajduje się w żadnym znanym wycieku danych. Powinno być bezpieczne." }, "baseDomain": { "message": "Domena podstawowa", "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Domena podstawowa (rekomendowana)", + "message": "Domena podstawowa (domyślne)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -2078,7 +2078,7 @@ "message": "Wszystkie elementy" }, "noPasswordsInList": { - "message": "Brak haseł." + "message": "Brak haseł do wyświetlenia." }, "clearHistory": { "message": "Wyczyść historię" @@ -2093,7 +2093,7 @@ "message": "Usuń" }, "default": { - "message": "Domyślny" + "message": "Domyślne" }, "dateUpdated": { "message": "Zaktualizowano", @@ -2135,7 +2135,7 @@ "description": "ex. A weak password. Scale: Weak -> Good -> Strong" }, "weakMasterPassword": { - "message": "Słabe hasło główne" + "message": "Hasło główne jest słabe" }, "weakMasterPasswordDesc": { "message": "Wybrane przez Ciebie hasło główne jest słabe. Powinieneś użyć silniejszego hasła (lub frazy), aby właściwie chronić swoje konto Bitwarden. Czy na pewno chcesz użyć tego hasła głównego?" @@ -2169,10 +2169,10 @@ "message": "Zbyt wiele nieprawidłowych prób wpisywania PIN. Wylogowywanie." }, "unlockWithBiometrics": { - "message": "Odblokuj danymi biometrycznymi" + "message": "Odblokuj biometrią" }, "unlockWithMasterPassword": { - "message": "Odblokuj za pomocą głównego hasła" + "message": "Odblokuj hasłem głównym" }, "awaitDesktop": { "message": "Oczekiwanie na potwierdzenie z aplikacji desktopowej" @@ -2199,7 +2199,7 @@ "message": "Generator hasła" }, "usernameGenerator": { - "message": "Generator nazw użytkownika" + "message": "Generator nazwy użytkownika" }, "useThisEmail": { "message": "Użyj tego adresu e-mail" @@ -2248,7 +2248,7 @@ "message": "Usuń trwale element" }, "permanentlyDeleteItemConfirmation": { - "message": "Czy na pewno chcesz usunąć trwale ten element?" + "message": "Czy na pewno chcesz usunąć trwale element?" }, "permanentlyDeletedItem": { "message": "Element został trwale usunięty" @@ -2266,13 +2266,13 @@ "message": "Po wylogowaniu się z sejfu musisz ponownie zalogować się, aby uzyskać do niego dostęp. Czy na pewno chcesz użyć tego ustawienia?" }, "vaultTimeoutLogOutConfirmationTitle": { - "message": "Potwierdź sposób blokowania sejfu" + "message": "Potwierdź sposób blokady" }, "autoFillAndSave": { - "message": "Automatycznie uzupełnij i zapisz" + "message": "Uzupełnij i zapisz" }, "fillAndSave": { - "message": "Wypełnij i zapisz" + "message": "Uzupełnij i zapisz" }, "autoFillSuccessAndSavedUri": { "message": "URI został zapisany i automatycznie uzupełniony" @@ -2302,7 +2302,7 @@ "message": "Ustaw hasło główne" }, "currentMasterPass": { - "message": "Aktualne hasło główne" + "message": "Obecne hasło główne" }, "newMasterPass": { "message": "Nowe hasło główne" @@ -2413,7 +2413,7 @@ "message": "Aplikacja desktopowa Bitwarden, przed odblokowaniem danymi biometrycznymi, musi zostać ponownie uruchomiona." }, "errorEnableBiometricTitle": { - "message": "Nie można włączyć danych biometrycznych" + "message": "Nie można włączyć biometrii" }, "errorEnableBiometricDesc": { "message": "Operacja została anulowana przez aplikację desktopową" @@ -2431,37 +2431,37 @@ "message": "Konto jest niezgodne" }, "nativeMessagingWrongUserKeyTitle": { - "message": "Klucz biometryczny jest niepoprawny" + "message": "Klucz biometrii jest nieprawidłowy" }, "nativeMessagingWrongUserKeyDesc": { "message": "Odblokowanie biometryczne się nie powiodło. Sekretny klucz biometryczny nie odblokował sejfu. Spróbuj skonfigurować biometrię ponownie." }, "biometricsNotEnabledTitle": { - "message": "Dane biometryczne są wyłączone" + "message": "Biometria jest wyłączona" }, "biometricsNotEnabledDesc": { "message": "Aby włączyć dane biometryczne w przeglądarce, musisz włączyć tę samą funkcję w ustawianiach aplikacji desktopowej." }, "biometricsNotSupportedTitle": { - "message": "Dane biometryczne nie są obsługiwane" + "message": "Biometria nie jest obsługiwana" }, "biometricsNotSupportedDesc": { - "message": "Dane biometryczne przeglądarki nie są obsługiwane na tym urządzeniu." + "message": "Biometria przeglądarki nie jest obsługiwana na tym urządzeniu." }, "biometricsNotUnlockedTitle": { - "message": "Użytkownik zablokowany lub wylogowany" + "message": "Użytkownik jest zablokowany lub wylogowany" }, "biometricsNotUnlockedDesc": { - "message": "Odblokuj tego użytkownika w aplikacji desktopowej i spróbuj ponownie." + "message": "Odblokuj użytkownika w aplikacji desktopowej i spróbuj ponownie." }, "biometricsNotAvailableTitle": { - "message": "Odblokowanie biometryczne jest niedostępne" + "message": "Odblokowanie biometrią jest niedostępne" }, "biometricsNotAvailableDesc": { - "message": "Odblokowanie biometryczne jest obecnie niedostępne. Spróbuj ponownie później." + "message": "Odblokowanie biometrią jest obecnie niedostępne. Spróbuj ponownie później." }, "biometricsFailedTitle": { - "message": "Dane biometryczne są błędne" + "message": "Logowanie biometrią nie powiodło się" }, "biometricsFailedDesc": { "message": "Dane biometryczne nie mogę być użyte, rozważ użycie hasła głównego lub wylogowanie. Jeśli się to powtarza, skontaktuj się z pomocą techniczną Bitwarden." @@ -2488,7 +2488,7 @@ "message": "Polityka organizacji zablokowała importowanie elementów do Twojego sejfu." }, "restrictCardTypeImport": { - "message": "Nie można importować elementów typu karty" + "message": "Nie można zaimportować karty" }, "restrictCardTypeImportDesc": { "message": "Polityka ustawiona przez 1 lub więcej organizacji uniemożliwia importowanie kart do sejfów." @@ -2498,10 +2498,10 @@ "description": "A category title describing the concept of web domains" }, "blockedDomains": { - "message": "Zablokowane domeny" + "message": "Blokowane domeny" }, "learnMoreAboutBlockedDomains": { - "message": "Dowiedz się więcej o zablokowanych domenach" + "message": "Dowiedz się więcej o blokowanych domenach" }, "excludedDomains": { "message": "Wykluczone domeny" @@ -2513,10 +2513,10 @@ "message": "Aplikacja Bitwarden nie będzie proponować zapisywania danych logowania dla tych domen dla wszystkich zalogowanych kont. Musisz odświeżyć stronę, aby zastosowywać zmiany." }, "blockedDomainsDesc": { - "message": "Autouzupełnianie i inne powiązane funkcje nie będą oferowane dla tych stron. Aby zmiany zaczęły obowiązywać, musisz odświeżyć stronę." + "message": "Autouzupełnianie będzie zablokowane dla tych stron internetowych. Zmiany zaczną obowiązywać po odświeżeniu strony." }, "autofillBlockedNoticeV2": { - "message": "Autouzupełnianie jest zablokowane dla tej witryny." + "message": "Autouzupełnianie będzie zablokowane dla tych stron intenretowych." }, "autofillBlockedNoticeGuidance": { "message": "Zmień to w ustawieniach" @@ -2544,7 +2544,7 @@ "message": "Zagrożone hasła" }, "atRiskPasswordDescSingleOrg": { - "message": "$ORGANIZATION$ prosi o zmianę jednego hasła, ponieważ jest ono zagrożone.", + "message": "Organizacja $ORGANIZATION$ prosi o zmianę 1 hasła, ponieważ jest ono zagrożone.", "placeholders": { "organization": { "content": "$1", @@ -2553,7 +2553,7 @@ } }, "atRiskPasswordsDescSingleOrgPlural": { - "message": "$ORGANIZATION$ prosi o zmianę $COUNT$ haseł, ponieważ są one zagrożone.", + "message": "Organizacja $ORGANIZATION$ prosi o zmianę $COUNT$ haseł, ponieważ są one zagrożone.", "placeholders": { "organization": { "content": "$1", @@ -2566,7 +2566,7 @@ } }, "atRiskPasswordsDescMultiOrgPlural": { - "message": "Twoje organizacje proszą o zmianę $COUNT$ haseł, ponieważ są one zagrożone.", + "message": "Twoja organizacja prosi o zmianę $COUNT$ haseł, ponieważ są one zagrożone.", "placeholders": { "count": { "content": "$1", @@ -2575,7 +2575,7 @@ } }, "atRiskChangePrompt": { - "message": "Twoje hasło dla tej witryny jest zagrożone. Organizacja $ORGANIZATION$ poprosiła Cię o jego zmianę.", + "message": "Hasło dla tej strony internetowej jest zagrożone. Organizacja $ORGANIZATION$ prosi o jego zmianę.", "placeholders": { "organization": { "content": "$1", @@ -2585,7 +2585,7 @@ "description": "Notification body when a login triggers an at-risk password change request and the change password domain is known." }, "atRiskNavigatePrompt": { - "message": "$ORGANIZATION$ chce, abyś zmienił to hasło, ponieważ jest ono zagrożone. Przejdź do ustawień konta, aby zmienić hasło.", + "message": "Organizacja $ORGANIZATION$ prosi o zmianę hasła, ponieważ jest ono zagrożone. Przejdź do ustawień konta, aby zmienić hasło.", "placeholders": { "organization": { "content": "$1", @@ -2595,7 +2595,7 @@ "description": "Notification body when a login triggers an at-risk password change request and no change password domain is provided." }, "reviewAndChangeAtRiskPassword": { - "message": "Przejrzyj i zmień jedno zagrożone hasło" + "message": "Sprawdź i zmień 1 zagrożone hasło" }, "reviewAndChangeAtRiskPasswordsPlural": { "message": "Przejrzyj i zmień $COUNT$ zagrożonych haseł ", @@ -2613,10 +2613,10 @@ "message": "Zaktualizuj swoje ustawienia, aby szybko autouzupełniać hasła i generować nowe" }, "reviewAtRiskLogins": { - "message": "Przejrzyj zagrożone loginy" + "message": "Sprawdź zagrożone dane logowania" }, "reviewAtRiskPasswords": { - "message": "Przejrzyj zagrożone hasła" + "message": "Sprawdź zagrożone hasła" }, "reviewAtRiskLoginsSlideDesc": { "message": "Twoje hasła organizacji są zagrożone, ponieważ są słabe, ponownie używane i/lub narażone.", @@ -2633,7 +2633,7 @@ "message": "Ilustracja menu autouzupełniania Bitwardena pokazująca wygenerowane hasło." }, "updateInBitwarden": { - "message": "Aktualizacja w Bitwarden" + "message": "Zaktualizuj w Bitwarden" }, "updateInBitwardenSlideDesc": { "message": "Bitwarden poprosi Cię o aktualizację hasła w menedżerze haseł.", @@ -2643,7 +2643,7 @@ "message": "Ilustracja powiadomienia Bitwardena, skłaniająca użytkownika do zaktualizowania danych logowania." }, "turnOnAutofill": { - "message": "Włącz autouzupełnienie" + "message": "Włącz autouzupełnianie" }, "turnedOnAutofill": { "message": "Włączono autouzupełnianie" @@ -2661,7 +2661,7 @@ } }, "excludedDomainsInvalidDomain": { - "message": "$DOMAIN$ nie jest prawidłową nazwą domeny", + "message": "Domena $DOMAIN$ nie jest prawidłowa", "placeholders": { "domain": { "content": "$1", @@ -2670,7 +2670,7 @@ } }, "blockedDomainsSavedSuccess": { - "message": "Zmiany w zablokowanych domenach zapisane" + "message": "Blokowane domeny zostały zapisane" }, "excludedDomainsSavedSuccess": { "message": "Zmiany w wykluczonych domenach zapisane" @@ -2693,18 +2693,18 @@ } }, "send": { - "message": "Wyślij", + "message": "Wysyłki", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDetails": { - "message": "Szczegóły Send", + "message": "Szczegóły wysyłki", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeText": { "message": "Tekst" }, "sendTypeTextToShare": { - "message": "Tekst do udostępnienia" + "message": "Tekst wysyłki" }, "sendTypeFile": { "message": "Plik" @@ -2718,13 +2718,13 @@ "description": "This text will be displayed after a Send has been accessed the maximum amount of times." }, "hideTextByDefault": { - "message": "Domyślnie ukryj tekst" + "message": "Ukryj domyślnie tekst wysyłki" }, "expired": { "message": "Wygasła" }, "passwordProtected": { - "message": "Chroniona hasłem" + "message": "Zabezpieczone hasłem" }, "copyLink": { "message": "Kopiuj link" @@ -2751,7 +2751,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "disabled": { - "message": "Wyłączona" + "message": "Wyłączone" }, "removePasswordConfirmation": { "message": "Czy na pewno chcesz usunąć hasło?" @@ -2761,11 +2761,11 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deleteSendConfirmation": { - "message": "Czy na pewno chcesz usunąć tę wysyłkę?", + "message": "Czy na pewno chcesz usunąć wysyłkę?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deleteSendPermanentConfirmation": { - "message": "Czy na pewno chcesz trwale usunąć to Send?", + "message": "Czy na pewno chcesz usunąć trwale wysyłkę?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editSend": { @@ -2795,7 +2795,7 @@ } }, "custom": { - "message": "Niestandardowe" + "message": "Niestandardowa" }, "sendPasswordDescV3": { "message": "Zabezpiecz tę wiadomość hasłem, które będzie wymagane, aby uzyskać do niej dostęp.", @@ -2821,7 +2821,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSendSuccessfully": { - "message": "Send został stworzony!", + "message": "Wysyłka została utworzona!", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHoursSingle": { @@ -2853,7 +2853,7 @@ } }, "sendLinkCopied": { - "message": "Link Send został skopiowany", + "message": "Link wysyłki został skopiowany", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editedSend": { @@ -2896,10 +2896,10 @@ "message": "Data i czas usunięcia są wymagane." }, "dateParsingError": { - "message": "Wystąpił błąd podczas zapisywania dat usunięcia i wygaśnięcia." + "message": "Wystąpił błąd podczas zapisywania daty usunięcia i wygaśnięcia." }, "hideYourEmail": { - "message": "Ukryj mój adres e-mail przed oglądającymi." + "message": "Ukryj mój adres e-mail przed odbiorcami." }, "passwordPrompt": { "message": "Potwierdź hasłem głównym" @@ -2908,17 +2908,20 @@ "message": "Potwierdź hasło główne" }, "passwordConfirmationDesc": { - "message": "Ta operacja jest chroniona. Aby kontynuować, wpisz ponownie hasło główne." + "message": "Operacja jest chroniona. Aby kontynuować, wpisz ponownie hasło główne." }, "emailVerificationRequired": { "message": "Weryfikacja adresu e-mail jest wymagana" }, "emailVerifiedV2": { - "message": "E-mail zweryfikowany" + "message": "Adres e-mail został zweryfikowany" }, "emailVerificationRequiredDesc": { "message": "Musisz zweryfikować adres e-mail, aby korzystać z tej funkcji. Adres możesz zweryfikować w sejfie internetowym." }, + "masterPasswordSuccessfullySet": { + "message": "Hasło główne zostało pomyślnie ustawione" + }, "updatedMasterPassword": { "message": "Hasło główne zostało zaktualizowane" }, @@ -2926,7 +2929,7 @@ "message": "Zaktualizuj hasło główne" }, "updateMasterPasswordWarning": { - "message": "Hasło główne zostało zmienione przez administratora Twojej organizacji. Musisz je zaktualizować, aby uzyskać dostęp do sejfu. Ta czynność spowoduje wylogowanie z bieżącej sesji, przez co konieczne będzie ponowne zalogowanie się. Aktywne sesje na innych urządzeniach mogą pozostać aktywne przez maksymalnie godzinę." + "message": "Hasło główne zostało zmienione przez administratora Twojej organizacji. Aby uzyskać dostęp do sejfu, zaktualizuj hasło główne. Kontynuowanie spowoduje wylogowanie z obecnej sesji i konieczność ponownego zalogowania się. Aktywne sesje na innych urządzeniach mogą pozostać aktywne przez maksymalnie godzinę." }, "updateWeakMasterPasswordWarning": { "message": "Twoje hasło główne nie spełnia jednej lub kilku zasad organizacji. Aby uzyskać dostęp do sejfu, musisz teraz zaktualizować swoje hasło główne. Kontynuacja wyloguje Cię z bieżącej sesji, wymagając zalogowania się ponownie. Aktywne sesje na innych urządzeniach mogą pozostać aktywne przez maksymalnie jedną godzinę." @@ -2938,7 +2941,7 @@ "message": "Automatyczne rejestrowanie użytkowników" }, "resetPasswordAutoEnrollInviteWarning": { - "message": "Ta organizacja posługuje się zasadą, która automatycznie rejestruje użytkowników do resetowania hasła. Rejestracja umożliwia administratorom organizacji zmianę Twojego hasła głównego." + "message": "Zasada organizacji umożliwia administratorom organizacji zmianę Twojego hasła głównego." }, "selectFolder": { "message": "Wybierz folder..." @@ -2956,7 +2959,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "cardMetrics": { - "message": "z $TOTAL$ elementów", + "message": "z $TOTAL$", "placeholders": { "total": { "content": "$1", @@ -2991,7 +2994,7 @@ } }, "vaultTimeoutPolicyInEffect1": { - "message": "$HOURS$ godzin(y) i $MINUTES$ minut(y) maksymalnie.", + "message": "Maksymalnie $HOURS$ godz. i $MINUTES$ min.", "placeholders": { "hours": { "content": "$1", @@ -3052,7 +3055,7 @@ "message": "Co najmniej jedna zasada organizacji uniemożliwia wyeksportowanie osobistego sejfu." }, "copyCustomFieldNameInvalidElement": { - "message": "Nie można zidentyfikować poprawnego elementu formularza. Spróbuj sprawdzić kod HTML." + "message": "Nie można zidentyfikować poprawnego elementu formularza. Spróbuj sprawdzić HTML." }, "copyCustomFieldNameNotUnique": { "message": "Nie znaleziono unikatowego identyfikatora." @@ -3064,7 +3067,7 @@ "message": "Nazwa organizacji" }, "keyConnectorDomain": { - "message": "Domena Key Connector'a" + "message": "Domena Key Connector" }, "leaveOrganization": { "message": "Opuść organizację" @@ -3076,13 +3079,13 @@ "message": "Hasło główne zostało usunięte" }, "leaveOrganizationConfirmation": { - "message": "Czy na pewno chcesz opuścić tę organizację?" + "message": "Czy na pewno chcesz opuścić organizację?" }, "leftOrganization": { - "message": "Nie należysz już do tej organizacji." + "message": "Opuszczono organizację." }, "toggleCharacterCount": { - "message": "Pokaż / Ukryj licznik znaków" + "message": "Pokaż / ukryj licznik znaków" }, "sessionTimeout": { "message": "Twoja sesja wygasła. Zaloguj się ponownie." @@ -3141,7 +3144,7 @@ "message": "Wygeneruj nazwę użytkownika" }, "generateEmail": { - "message": "Wygeneruj e-mail" + "message": "Wygeneruj adres e-mail" }, "spinboxBoundariesHint": { "message": "Wartość musi być pomiędzy $MIN$ a $MAX$.", @@ -3158,7 +3161,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Użyj $RECOMMENDED$ znaków lub więcej, aby wygenerować silne hasło.", + "message": "Użyj co najmniej $RECOMMENDED$ znaków, aby utworzyć silne hasło.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -3168,7 +3171,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Użyj $RECOMMENDED$ słów lub więcej, aby wygenerować silne hasło.", + "message": "Użyj co najmniej $RECOMMENDED$ słów, aby wygenerować silne hasło wyrazowe.", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -3191,13 +3194,13 @@ "message": "Użyj skonfigurowanej skrzynki catch-all w swojej domenie." }, "random": { - "message": "Losowa" + "message": "Losowe" }, "randomWord": { "message": "Losowe słowo" }, "websiteName": { - "message": "Nazwa strony" + "message": "Nazwa strony internetowej" }, "service": { "message": "Usługa" @@ -3206,7 +3209,7 @@ "message": "Alias przekierowania" }, "forwardedEmailDesc": { - "message": "Wygeneruj alias adresu e-mail z zewnętrznej usługi przekierowania." + "message": "Wygeneruj alias za pomocą zewnętrznej usługi." }, "forwarderDomainName": { "message": "Domena adresu e-mail", @@ -3245,7 +3248,7 @@ } }, "forwaderInvalidToken": { - "message": "Nieprawidłowy token API dla $SERVICENAME$", + "message": "Token API $SERVICENAME$ jest nieprawidłowy", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -3255,7 +3258,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Nieprawidłowy token API dla $SERVICENAME$, błąd: $ERRORMESSAGE$", + "message": "Token API $SERVICENAME$ jest nieprawidłowy: $ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -3303,7 +3306,7 @@ } }, "forwarderNoDomain": { - "message": "Nieprawidłowa domena $SERVICENAME$.", + "message": "Domena $SERVICENAME$ jest nieprawidłowa.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -3313,7 +3316,7 @@ } }, "forwarderNoUrl": { - "message": "Nieprawidłowy adres URL $SERVICENAME$.", + "message": "Adres URL $SERVICENAME$ jest nieprawidłowy.", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -3323,7 +3326,7 @@ } }, "forwarderUnknownError": { - "message": "Wystąpił nieznany błąd w $SERVICENAME$.", + "message": "Wystąpił nieznany błąd $SERVICENAME$.", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -3356,7 +3359,7 @@ "message": "Błąd serwera Key Connector: upewnij się, że serwer Key Connector jest dostępny i działa poprawnie." }, "premiumSubcriptionRequired": { - "message": "Wymagana jest subskrypcja Premium" + "message": "Wymagana jest subskrypcja premium" }, "organizationIsDisabled": { "message": "Organizacja została zawieszona." @@ -3365,7 +3368,7 @@ "message": "Nie można uzyskać dostępu do elementów w zawieszonych organizacjach. Skontaktuj się z właścicielem organizacji, aby uzyskać pomoc." }, "loggingInTo": { - "message": "Logowanie do $DOMAIN$", + "message": "Logowanie na $DOMAIN$", "placeholders": { "domain": { "content": "$1", @@ -3428,7 +3431,7 @@ "message": "Powiadomienie zostało wysłane na urządzenie." }, "notificationSentDevicePart1": { - "message": "Odblokuj Bitwarden na swoim urządzeniu lub w" + "message": "Odblokuj Bitwarden na urządzeniu lub w" }, "notificationSentDeviceAnchor": { "message": "aplikacji internetowej" @@ -3437,13 +3440,13 @@ "message": "Upewnij się, że fraza odcisku palca zgadza się z tą poniżej, zanim zatwierdzisz." }, "aNotificationWasSentToYourDevice": { - "message": "Powiadomienie zostało wysłane na twoje urządzenie" + "message": "Powiadomienie zostało wysłane na urządzenie" }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "Zostaniesz powiadomiony po zatwierdzeniu prośby" }, "needAnotherOptionV1": { - "message": "Potrzebujesz innego sposobu?" + "message": "Potrzebujesz innej opcji?" }, "loginInitiated": { "message": "Logowanie rozpoczęte" @@ -3452,13 +3455,13 @@ "message": "Żądanie wysłane" }, "exposedMasterPassword": { - "message": "Ujawnione hasło główne" + "message": "Hasło główne zostało ujawnione" }, "exposedMasterPasswordDesc": { "message": "Hasło ujawnione w wyniku naruszenia ochrony danych. Użyj unikalnego hasła, aby chronić swoje konto. Czy na pewno chcesz użyć ujawnionego hasła?" }, "weakAndExposedMasterPassword": { - "message": "Słabe i ujawnione hasło główne" + "message": "Hasło główne jest słabe i ujawnione" }, "weakAndBreachedMasterPasswordDesc": { "message": "Słabe hasło ujawnione w wyniku naruszenia ochrony danych. Użyj silnego i unikalnego hasła, aby chronić swoje konto. Czy na pewno chcesz użyć tego hasła?" @@ -3488,7 +3491,7 @@ "message": "Jak autouzupełniać" }, "autofillSelectInfoWithCommand": { - "message": "Wybierz element z tego ekranu, użyj skrótu $COMMAND$ lub zobacz inne opcje w ustawieniach.", + "message": "Wybierz element, użyj skrótu $COMMAND$ lub zobacz inne opcje w ustawieniach.", "placeholders": { "command": { "content": "$1", @@ -3500,7 +3503,7 @@ "message": "Wybierz element z tego ekranu lub zobacz inne opcje w ustawieniach." }, "gotIt": { - "message": "Rozumiem" + "message": "Ok" }, "autofillSettings": { "message": "Ustawienia autouzupełniania" @@ -3515,13 +3518,13 @@ "message": "Zarządzaj skrótami" }, "autofillShortcut": { - "message": "Skrót klawiaturowy autouzupełniania" + "message": "Skrót klawiszowy autouzupełniania" }, "autofillLoginShortcutNotSet": { "message": "Skrót autouzupełniania nie jest ustawiony. Zmień to w ustawieniach przeglądarki." }, "autofillLoginShortcutText": { - "message": "Skrót autouzupełniania to: $COMMAND$. Zmień to w ustawieniach przeglądarki.", + "message": "Skrót autouzupełniania to $COMMAND$. Zmień skróty w ustawieniach przeglądarki.", "placeholders": { "command": { "content": "$1", @@ -3554,7 +3557,7 @@ "message": "Wybierz opcję zatwierdzenia poniżej" }, "rememberThisDevice": { - "message": "Zapamiętaj to urządzenie" + "message": "Zapamiętaj urządzenie" }, "uncheckIfPublicDevice": { "message": "Odznacz, jeśli używasz publicznego urządzenia" @@ -3603,7 +3606,7 @@ "message": "Wyświetl" }, "accountSuccessfullyCreated": { - "message": "Konto pomyślnie utworzone!" + "message": "Konto zostało utworzone!" }, "adminApprovalRequested": { "message": "Poproszono administratora o zatwierdzenie" @@ -3612,10 +3615,10 @@ "message": "Twoja prośba została wysłana do Twojego administratora." }, "troubleLoggingIn": { - "message": "Problem z zalogowaniem?" + "message": "Problem z logowaniem?" }, "loginApproved": { - "message": "Logowanie zatwierdzone" + "message": "Logowanie zostało potwierdzone" }, "userEmailMissing": { "message": "Brak adresu e-mail użytkownika" @@ -3713,7 +3716,7 @@ } }, "multipleInputEmails": { - "message": "Co najmniej 1 e-mail jest nieprawidłowy" + "message": "Co najmniej 1 adres e-mail jest nieprawidłowy" }, "inputTrimValidator": { "message": "Tekst nie może zawierać tylko spacji.", @@ -3723,7 +3726,7 @@ "message": "Dane wejściowe nie są adresem e-mail." }, "fieldsNeedAttention": { - "message": "Pola powyżej wymagające Twojej uwagi: $COUNT$.", + "message": "Pola wymagające uwagi: $COUNT$.", "placeholders": { "count": { "content": "$1", @@ -3732,10 +3735,10 @@ } }, "singleFieldNeedsAttention": { - "message": "1 pole wymaga Twojej uwagi." + "message": "1 pole wymaga uwagi." }, "multipleFieldsNeedAttention": { - "message": "Pola wymagające Twojej uwagi: $COUNT$.", + "message": "Pola wymagające uwagi: $COUNT$.", "placeholders": { "count": { "content": "$1", @@ -3753,7 +3756,7 @@ "message": "Pobieranie opcji..." }, "multiSelectNotFound": { - "message": "Nie znaleziono żadnych pozycji" + "message": "Nie znaleziono elementów" }, "multiSelectClearAll": { "message": "Wyczyść wszystko" @@ -3812,7 +3815,7 @@ "description": "Text to display in overlay when the account is locked." }, "unlockYourAccountToViewAutofillSuggestions": { - "message": "Odblokuj swoje konto, aby zobaczyć sugestie autouzupełniania", + "message": "Odblokuj konto, aby zobaczyć sugestie autouzupełniania", "description": "Text to display in overlay when the account is locked." }, "unlockAccount": { @@ -3832,7 +3835,7 @@ "description": "Aria label for the totp seconds displayed in the inline menu for autofill" }, "fillCredentialsFor": { - "message": "Wypełnij dane logowania dla", + "message": "Uzupełnij dane logowania dla", "description": "Screen reader text for when overlay item is in focused" }, "partialUsername": { @@ -3890,7 +3893,7 @@ "description": "Used for the header of the import dialog, the import button and within the file-password-prompt" }, "importError": { - "message": "Błąd importu" + "message": "Błąd importowania" }, "importErrorDesc": { "message": "Wystąpił problem z danymi, które chcesz zaimportować. Rozwiąż poniższe problemy w Twoim pliku i spróbuj ponownie." @@ -3902,7 +3905,7 @@ "message": "Opis" }, "importSuccess": { - "message": "Importowanie danych zakończone sukcesem" + "message": "Dane zostały zaimportowane" }, "importSuccessNumberOfItems": { "message": "Zaimportowano elementów: $AMOUNT$.", @@ -3920,10 +3923,10 @@ "message": "Weryfikacja dla tej akcji jest wymagana. Ustaw kod PIN, aby kontynuować." }, "setPin": { - "message": "Ustaw PIN" + "message": "Ustaw kod PIN" }, "verifyWithBiometrics": { - "message": "Weryfikuj za pomocą biometrii" + "message": "Zweryfikuj biometrią" }, "awaitingConfirmation": { "message": "Oczekiwanie na potwierdzenie" @@ -3938,7 +3941,7 @@ "message": "Użyj hasła głównego" }, "usePin": { - "message": "Użyj PINu" + "message": "Użyj kodu PIN" }, "useBiometrics": { "message": "Użyj biometrii" @@ -3947,7 +3950,7 @@ "message": "Wpisz kod weryfikacyjny, który został wysłany na adres e-mail." }, "resendCode": { - "message": "Wysłać kod ponownie" + "message": "Wyślij ponownie kod" }, "total": { "message": "Łącznie" @@ -3965,13 +3968,13 @@ "message": "Wystąpił błąd podczas połączenia z usługą Duo. Aby uzyskać pomoc, użyj innej metody dwustopniowego logowania lub skontaktuj się z Duo." }, "duoRequiredForAccount": { - "message": "Dwustopniowe logowanie Duo jest wymagane dla Twojego konta." + "message": "Konto wymaga logowania dwustopniowego Duo." }, "popoutExtension": { "message": "Otwórz rozszerzenie w nowym oknie" }, "launchDuo": { - "message": "Uruchom DUO" + "message": "Uruchom Duo" }, "importFormatError": { "message": "Dane nie są poprawnie sformatowane. Sprawdź importowany plik i spróbuj ponownie." @@ -4023,7 +4026,7 @@ "message": "Nie wybrano pliku" }, "orCopyPasteFileContents": { - "message": "lub skopiuj/wklej treść pliku" + "message": "lub wklej zawartość pliku" }, "instructionsFor": { "message": "Instrukcja dla $NAME$", @@ -4051,7 +4054,7 @@ "message": "Klucz dostępu" }, "accessing": { - "message": "Uzyskiwanie dostępu" + "message": "Logowanie na" }, "loggedInExclamation": { "message": "Zalogowano!" @@ -4066,7 +4069,7 @@ "message": "Weryfikacja jest wymagana przez stronę inicjującą. Ta funkcja nie jest jeszcze zaimplementowana dla kont bez hasła głównego." }, "logInWithPasskeyQuestion": { - "message": "Zalogować za pomocą klucza dostępu?" + "message": "Zalogować się kluczem dostępu?" }, "passkeyAlreadyExists": { "message": "Klucz dostępu już istnieje dla tej aplikacji." @@ -4105,7 +4108,7 @@ "message": "Zastąpić klucz dostępu?" }, "overwritePasskeyAlert": { - "message": "Ten element zawiera już klucz dostępu. Czy na pewno chcesz nadpisać bieżący klucza dostępu?" + "message": "Element zawiera już klucz dostępu. Czy na pewno chcesz zastąpić obecny klucz dostępu?" }, "featureNotSupported": { "message": "Funkcja nie jest jeszcze obsługiwana" @@ -4123,13 +4126,13 @@ "message": "Nieprawidłowa nazwa użytkownika lub hasło" }, "incorrectPassword": { - "message": "Błędne hasło" + "message": "Hasło jest nieprawidłowe" }, "incorrectCode": { "message": "Błędny kod" }, "incorrectPin": { - "message": "Niepoprawny PIN" + "message": "Kod PIN jest nieprawidłowy" }, "multifactorAuthenticationFailed": { "message": "Uwierzytelnianie wieloskładnikowe nie powiodło się" @@ -4138,7 +4141,7 @@ "message": "Dołącz udostępnione foldery" }, "lastPassEmail": { - "message": "E-mail LastPass" + "message": "Adres e-mail LastPass" }, "importingYourAccount": { "message": "Importowanie konta..." @@ -4244,12 +4247,32 @@ "message": "Popularne formaty", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { - "message": "Kontynuować do ustawień przeglądarki?", + "message": "Przejść do ustawień przeglądarki?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" }, "confirmContinueToHelpCenter": { - "message": "Kontynuować do centrum pomocy?", + "message": "Przejść do centrum pomocy?", "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" }, "confirmContinueToHelpCenterPasswordManagementContent": { @@ -4257,7 +4280,7 @@ "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser password management settings" }, "confirmContinueToHelpCenterKeyboardShortcutsContent": { - "message": "Możesz przeglądać i ustawiać skróty klawiaturowe rozszerzeń w ustawieniach przeglądarki.", + "message": "Możesz zmieniać skróty rozszerzenia w ustawieniach przeglądarki.", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser keyboard shortcut settings" }, "confirmContinueToBrowserPasswordManagementSettingsContent": { @@ -4265,7 +4288,7 @@ "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" }, "confirmContinueToBrowserKeyboardShortcutSettingsContent": { - "message": "Możesz przeglądać i ustawiać skróty klawiaturowe rozszerzeń w ustawieniach przeglądarki.", + "message": "Możesz zmieniać skróty rozszerzenia w ustawieniach przeglądarki.", "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" }, "overrideDefaultBrowserAutofillTitle": { @@ -4301,7 +4324,7 @@ "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { - "message": "Pomyślnie zaktualizowano dane logowania!", + "message": "Dane logowania zostały zaktualizowane!", "description": "Notification message for when updating credentials has succeeded." }, "passwordUpdated": { @@ -4331,7 +4354,7 @@ "message": "Zapisz element logowania dla tej witryny, aby automatycznie wypełnić" }, "yourVaultIsEmpty": { - "message": "Twój sejf jest pusty" + "message": "Sejf jest pusty" }, "noItemsMatchSearch": { "message": "Żaden element nie pasuje do Twojego wyszukiwania" @@ -4340,7 +4363,7 @@ "message": "Wyczyść filtry lub użyj innej frazy" }, "copyInfoTitle": { - "message": "Skopiuj informacje - $ITEMNAME$", + "message": "Kopiuj informacje - $ITEMNAME$", "description": "Title for a button that opens a menu with options to copy information from an item.", "placeholders": { "itemname": { @@ -4350,7 +4373,7 @@ } }, "copyNoteTitle": { - "message": "Skopiuj notatkę - $ITEMNAME$", + "message": "Kopiuj notatkę - $ITEMNAME$", "description": "Title for a button copies a note to the clipboard.", "placeholders": { "itemname": { @@ -4380,7 +4403,7 @@ } }, "viewItemTitle": { - "message": "Zobacz element - $ITEMNAME$", + "message": "Pokaż element - $ITEMNAME$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -4390,7 +4413,7 @@ } }, "viewItemTitleWithField": { - "message": "Zobacz element - $ITEMNAME$ - $FIELD$", + "message": "Pokaż element - $ITEMNAME$ - $FIELD$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -4404,7 +4427,7 @@ } }, "autofillTitle": { - "message": "Autouzupełnij - $ITEMNAME$", + "message": "Uzupełnij - $ITEMNAME$", "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { @@ -4414,7 +4437,7 @@ } }, "autofillTitleWithField": { - "message": "Autouzupełnij - $ITEMNAME$ - $FIELD$", + "message": "Uzupełnij - $ITEMNAME$ - $FIELD$", "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { @@ -4448,16 +4471,16 @@ "message": "Przypisz do kolekcji" }, "copyEmail": { - "message": "Skopiuj e-mail" + "message": "Kopiuj adres e-mail" }, "copyPhone": { - "message": "Skopiuj telefon" + "message": "Kopiuj numer telefonu" }, "copyAddress": { - "message": "Skopiuj adres" + "message": "Kopiuj adres" }, "adminConsole": { - "message": "Konsola Administracyjna" + "message": "Konsola administratora" }, "accountSecurity": { "message": "Bezpieczeństwo konta" @@ -4475,7 +4498,7 @@ "message": "Wystąpił błąd podczas przypisywania folderu." }, "viewItemsIn": { - "message": "Zobacz elementy w $NAME$", + "message": "Pokaż elementy w $NAME$", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -4508,7 +4531,7 @@ } }, "itemsWithNoFolder": { - "message": "Elementy bez folderu" + "message": "Nieprzypisane" }, "itemDetails": { "message": "Szczegóły elementu" @@ -4536,19 +4559,19 @@ "message": "Historia elementu" }, "lastEdited": { - "message": "Ostatnio edytowany" + "message": "Zaktualizowano" }, "ownerYou": { "message": "Właściciel: Ty" }, "linked": { - "message": "Powiązane" + "message": "Powiązane pole" }, "copySuccessful": { "message": "Kopiowanie zakończone sukcesem" }, "upload": { - "message": "Wyślij" + "message": "Prześlij" }, "addAttachment": { "message": "Dodaj załącznik" @@ -4593,7 +4616,7 @@ "message": "Uzyskaj dostęp do sejfu bez przeglądarki, a następnie ustaw odblokowanie biometryczne, aby przyspieszyć odblokowanie zarówno w aplikacji desktopowej, jak i w rozszerzeniu przeglądarki." }, "downloadFromBitwardenNow": { - "message": "Pobierz teraz z bitwarden.com" + "message": "Pobierz z bitwarden.com" }, "getItOnGooglePlay": { "message": "Pobierz z Google Play" @@ -4602,7 +4625,7 @@ "message": "Pobierz z App Store" }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Czy na pewno chcesz trwale usunąć ten załącznik?" + "message": "Czy na pewno chcesz usunąć trwale załącznik?" }, "premium": { "message": "Premium" @@ -4632,7 +4655,7 @@ "message": "Dane osobowe" }, "identification": { - "message": "Tożsamość" + "message": "Identyfikacja" }, "contactInfo": { "message": "Daje kontaktowe" @@ -4673,7 +4696,7 @@ } }, "websiteAdded": { - "message": "Strona dodana" + "message": "Strona internetowa została dodana" }, "addWebsite": { "message": "Dodaj stronę internetową" @@ -4682,7 +4705,7 @@ "message": "Usuń stronę internetową" }, "defaultLabel": { - "message": "Domyślnie ($VALUE$)", + "message": "Domyślne ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -4692,7 +4715,7 @@ } }, "showMatchDetection": { - "message": "Pokaż wykrywanie dopasowań $WEBSITE$", + "message": "Pokaż wykrywanie dopasowania $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -4701,7 +4724,7 @@ } }, "hideMatchDetection": { - "message": "Ukryj wykrywanie dopasowań $WEBSITE$", + "message": "Ukryj wykrywanie dopasowania $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -4710,7 +4733,7 @@ } }, "autoFillOnPageLoad": { - "message": "Włącz autouzupełnianie po załadowaniu strony?" + "message": "Włączyć autouzupełnianie po załadowaniu strony?" }, "cardExpiredTitle": { "message": "Karta wygasła" @@ -4740,7 +4763,7 @@ "message": "Dodaj konto" }, "loading": { - "message": "Wczytywanie" + "message": "Ładowanie" }, "data": { "message": "Dane" @@ -4754,7 +4777,7 @@ "description": "A section header for a list of passwords." }, "logInWithPasskeyAriaLabel": { - "message": "Zaloguj się za pomocą klucza dostępu", + "message": "Logowaniem kluczem dostępu", "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { @@ -4785,7 +4808,7 @@ "message": "Dodaj" }, "fieldType": { - "message": "Typ pola" + "message": "Rodzaj pola" }, "fieldLabel": { "message": "Etykieta pola" @@ -4908,7 +4931,7 @@ "message": "Nie zaznaczyłeś żadnych elementów." }, "itemsMovedToOrg": { - "message": "Elementy przeniesione do $ORGNAME$", + "message": "Elementy zostały przeniesione do organizacji $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4917,7 +4940,7 @@ } }, "itemMovedToOrg": { - "message": "Element przeniesiony do $ORGNAME$", + "message": "Element został przeniesiony do organizacji $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4946,13 +4969,13 @@ "message": "Lokalizacja elementu" }, "fileSend": { - "message": "Wysyłka pliku" + "message": "Plik wysyłki" }, "fileSends": { "message": "Wysyłki plików" }, "textSend": { - "message": "Wysyłka tekstu" + "message": "Tekst wysyłki" }, "textSends": { "message": "Wysyłki tekstów" @@ -4964,7 +4987,7 @@ "message": "Pokaż liczbę sugestii autouzupełniania logowania na ikonie rozszerzenia" }, "showQuickCopyActions": { - "message": "Pokaż akcje szybkiego kopiowania w Sejfie" + "message": "Pokaż akcje szybkiego kopiowania w sejfie" }, "systemDefault": { "message": "Domyślny systemu" @@ -4979,22 +5002,22 @@ "message": "Klucz publiczny" }, "sshFingerprint": { - "message": "Odcisk palca" + "message": "Odcisk klucza" }, "sshKeyAlgorithm": { - "message": "Typ klucza" + "message": "Rodzaj klucza" }, "sshKeyAlgorithmED25519": { "message": "ED25519" }, "sshKeyAlgorithmRSA2048": { - "message": "RSA 2048-bitowy" + "message": "RSA 2048-bit" }, "sshKeyAlgorithmRSA3072": { - "message": "RSA 3072-bitowy" + "message": "RSA 3072-bit" }, "sshKeyAlgorithmRSA4096": { - "message": "RSA 4096-bitowy" + "message": "RSA 4096-bit" }, "retry": { "message": "Powtórz" @@ -5036,19 +5059,19 @@ "message": "Nie masz uprawnień do edycji tego elementu" }, "biometricsStatusHelptextUnlockNeeded": { - "message": "Odblokowanie odciskiem palca jest niedostępne, ponieważ najpierw wymagane jest odblokowanie kodem PIN lub hasłem." + "message": "Odblokowanie biometrią jest niedostępne, ponieważ najpierw wymagane jest odblokowanie kodem PIN lub hasłem." }, "biometricsStatusHelptextHardwareUnavailable": { - "message": "Odblokowanie biometryczne jest obecnie niedostępne." + "message": "Odblokowanie biometrią jest obecnie niedostępne." }, "biometricsStatusHelptextAutoSetupNeeded": { - "message": "Odblokowanie biometryczne jest niedostępne z powodu nieprawidłowej konfiguracji plików systemowych." + "message": "Odblokowanie biometrią jest niedostępne z powodu nieprawidłowej konfiguracji plików systemowych." }, "biometricsStatusHelptextManualSetupNeeded": { - "message": "Odblokowanie biometryczne jest niedostępne z powodu nieprawidłowej konfiguracji plików systemowych." + "message": "Odblokowanie biometrią jest niedostępne z powodu nieprawidłowej konfiguracji plików systemowych." }, "biometricsStatusHelptextDesktopDisconnected": { - "message": "Odblokowanie odciskiem palca jest niedostępne, ponieważ aplikacja desktopowa Bitwarden jest zamknięta." + "message": "Odblokowanie biometrią jest niedostępne z powodu zamkniętej aplikacji desktopowej Bitwarden." }, "biometricsStatusHelptextNotEnabledInDesktop": { "message": "Odblokowanie biometryczne jest niedostępne, ponieważ nie jest włączone dla $EMAIL$ w aplikacji desktopowej Bitwarden.", @@ -5063,16 +5086,16 @@ "message": "Odblokowanie biometryczne jest obecnie niedostępne z nieznanego powodu." }, "unlockVault": { - "message": "Odblokuj swój sejf w kilka sekund" + "message": "Odblokuj sejf w kilka sekund" }, "unlockVaultDesc": { "message": "Możesz dostosować ustawienia odblokowania i limitu czasu, aby szybciej uzyskać dostęp do sejfu." }, "unlockPinSet": { - "message": "Ustaw kod PIN odblokowujący" + "message": "Kod PIN został ustawiony" }, "unlockWithBiometricSet": { - "message": "Odblokuj za pomocą danych biometrycznych" + "message": "Odblokuj biometrią" }, "authenticating": { "message": "Uwierzytelnianie" @@ -5110,23 +5133,23 @@ "description": "Represents the @ key in screen reader content as a readable word" }, "hashSignCharacterDescriptor": { - "message": "Hashtag", + "message": "Kratka", "description": "Represents the # key in screen reader content as a readable word" }, "dollarSignCharacterDescriptor": { - "message": "Znak dolara", + "message": "Dolar", "description": "Represents the $ key in screen reader content as a readable word" }, "percentSignCharacterDescriptor": { - "message": "Znak procenta", + "message": "Procent", "description": "Represents the % key in screen reader content as a readable word" }, "caretCharacterDescriptor": { - "message": "Daszek", + "message": "Kareta", "description": "Represents the ^ key in screen reader content as a readable word" }, "ampersandCharacterDescriptor": { - "message": "Ampersand", + "message": "Et", "description": "Represents the & key in screen reader content as a readable word" }, "asteriskCharacterDescriptor": { @@ -5134,15 +5157,15 @@ "description": "Represents the * key in screen reader content as a readable word" }, "parenLeftCharacterDescriptor": { - "message": "Prawy nawias okrągły", + "message": "Lewy nawias", "description": "Represents the ( key in screen reader content as a readable word" }, "parenRightCharacterDescriptor": { - "message": "Prawy nawias okrągły", + "message": "Prawy nawias", "description": "Represents the ) key in screen reader content as a readable word" }, "hyphenCharacterDescriptor": { - "message": "Znak podkreślenia", + "message": "Podkreślnik", "description": "Represents the _ key in screen reader content as a readable word" }, "underscoreCharacterDescriptor": { @@ -5154,7 +5177,7 @@ "description": "Represents the + key in screen reader content as a readable word" }, "equalsCharacterDescriptor": { - "message": "Znak równości", + "message": "Równa się", "description": "Represents the = key in screen reader content as a readable word" }, "braceLeftCharacterDescriptor": { @@ -5174,11 +5197,11 @@ "description": "Represents the ] key in screen reader content as a readable word" }, "pipeCharacterDescriptor": { - "message": "Pionowa kreska", + "message": "Kreska pionowa", "description": "Represents the | key in screen reader content as a readable word" }, "backSlashCharacterDescriptor": { - "message": "Ukośnik wsteczny", + "message": "Ukośnik lewy", "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { @@ -5198,11 +5221,11 @@ "description": "Represents the ' key in screen reader content as a readable word" }, "lessThanCharacterDescriptor": { - "message": "Mniejszy niż", + "message": "Mniej niż", "description": "Represents the < key in screen reader content as a readable word" }, "greaterThanCharacterDescriptor": { - "message": "Większy niż", + "message": "Więcej niż", "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { @@ -5255,22 +5278,22 @@ "message": "Potwierdź hasło" }, "enterSshKeyPasswordDesc": { - "message": "Wprowadź hasło dla klucza SSH." + "message": "Wpisz hasło klucza SSH." }, "enterSshKeyPassword": { - "message": "Wprowadź hasło" + "message": "Wpisz hasło" }, "invalidSshKey": { "message": "Klucz SSH jest nieprawidłowy" }, "sshKeyTypeUnsupported": { - "message": "Typ klucza SSH nie jest obsługiwany" + "message": "Ten klucz SSH nie jest obsługiwany" }, "importSshKeyFromClipboard": { "message": "Importuj klucz ze schowka" }, "sshKeyImported": { - "message": "Klucz SSH zaimportowano pomyślnie" + "message": "Klucz SSH został zaimportowany" }, "cannotRemoveViewOnlyCollections": { "message": "Nie można usunąć kolekcji z uprawnieniami tylko do przeglądania: $COLLECTIONS$", @@ -5282,13 +5305,13 @@ } }, "updateDesktopAppOrDisableFingerprintDialogTitle": { - "message": "Zaktualizuj aplikację na komputer" + "message": "Zaktualizuj aplikację desktopową" }, "updateDesktopAppOrDisableFingerprintDialogMessage": { "message": "Aby używać odblokowywania biometrycznego, zaktualizuj aplikację na komputerze lub wyłącz odblokowywanie odciskiem palca w ustawieniach aplikacji na komputerze." }, "changeAtRiskPassword": { - "message": "Zmień hasło zagrożone" + "message": "Zmień zagrożone hasło" }, "settingsVaultOptions": { "message": "Ustawienia Sejfu" @@ -5300,43 +5323,43 @@ "message": "Witaj w Bitwarden" }, "securityPrioritized": { - "message": "Bezpieczeństwo priorytetem" + "message": "Priorytetowe bezpieczeństwo" }, "securityPrioritizedBody": { - "message": "Zapisz dane logowania, karty i tożsamości w bezpiecznym sejfie. Bitwarden stosuje szyfrowanie end-to-end z wiedzą zerową, aby chronić to, co jest dla Ciebie ważne." + "message": "Zapisz dane logowania, karty i tożsamości w bezpiecznym sejfie. Bitwarden używa szyfrowania end-to-end w celu ochrony tego, co jest dla Ciebie ważne." }, "quickLogin": { "message": "Szybkie i łatwe logowanie" }, "quickLoginBody": { - "message": "Skonfiguruj odblokowanie i autouzupełnianie biometryczne, aby zalogować się na swoje konta bez wpisywania pojedynczej litery." + "message": "Skonfiguruj odblokowywanie biometryczne i autouzupełnianie, aby logować się do swoich kont bez wpisywania nawet jednej litery." }, "secureUser": { - "message": "Ulepsz swoje loginy" + "message": "Ulepsz swoje dane logowania" }, "secureUserBody": { - "message": "Użyj generatora do tworzenia i zapisywania silnych, unikalnych haseł dla wszystkich kont." + "message": "Użyj generatora, aby utworzyć i zapisać silne, unikalne hasła do wszystkich swoich kont." }, "secureDevices": { "message": "Twoje dane, kiedy i gdzie potrzebujesz" }, "secureDevicesBody": { - "message": "Zapisuj nieograniczoną liczbę haseł na nieograniczonej liczbie urządzeń dzięki aplikacjom Bitwarden na urządzenia mobilne, przeglądarki i komputery stacjonarne." + "message": "Zapisuj nieograniczoną liczbę haseł dzięki aplikacjom Bitwarden, które są dostępne na urządzeniach mobilnych, w przeglądarkach i na komputerach." }, "nudgeBadgeAria": { "message": "1 powiadomienie" }, "emptyVaultNudgeTitle": { - "message": "Importuj istniejące hasła" + "message": "Importuj obecne hasła" }, "emptyVaultNudgeBody": { - "message": "Użyj importera, aby szybko przenieść loginy do Bitwarden bez ręcznego dodawania ich." + "message": "Przenieś dane logowania do Bitwarden w sposób automatyczny." }, "emptyVaultNudgeButton": { "message": "Importuj teraz" }, "hasItemsVaultNudgeTitle": { - "message": "Witaj w Twoim Sejfie!" + "message": "Witaj w sejfie!" }, "hasItemsVaultNudgeBodyOne": { "message": "Autouzupełnianie elementów dla bieżącej strony" @@ -5351,17 +5374,17 @@ "message": "Oszczędzaj czas dzięki autouzupełnianiu" }, "newLoginNudgeBodyOne": { - "message": "Dołącz", + "message": "Dodaj", "description": "This is in multiple parts to allow for bold text in the middle of the sentence.", "example": "Include a Website so this login appears as an autofill suggestion." }, "newLoginNudgeBodyBold": { - "message": "stronę internetową, ", + "message": "stronę internetową", "description": "This is in multiple parts to allow for bold text in the middle of the sentence.", "example": "Include a Website so this login appears as an autofill suggestion." }, "newLoginNudgeBodyTwo": { - "message": "aby ten login pojawił się jako sugestia autouzupełniania.", + "message": ", aby dane logowania pojawiały się jako sugestia autouzupełniania.", "description": "This is in multiple parts to allow for bold text in the middle of the sentence.", "example": "Include a Website so this login appears as an autofill suggestion." }, @@ -5369,25 +5392,25 @@ "message": "Bezproblemowe zamówienia online" }, "newCardNudgeBody": { - "message": "Z kartami łatwe autouzupełnianie formularzy płatności w sposób bezpieczny i dokładny." + "message": "Dzięki kartom możesz łatwo i bezpiecznie uzupełniać formularze płatności." }, "newIdentityNudgeTitle": { - "message": "Uprość tworzenie kont" + "message": "Łatwe tworzenie kont" }, "newIdentityNudgeBody": { - "message": "Z tożsamościami, szybko autouzupełnij długie formularze rejestracyjne lub kontaktowe." + "message": "Dzięki tożsamościom możesz szybko uzupełniać długie formularze rejestracyjne i kontaktowe." }, "newNoteNudgeTitle": { - "message": "Zachowaj bezpieczeństwo wrażliwych danych" + "message": "Chroń wrażliwe dane" }, "newNoteNudgeBody": { - "message": "Z notatkami bezpiecznie przechowuj dane szczególnie chronione, takie jak dane bankowe lub ubezpieczeniowe." + "message": "Dzięki notatkom możesz bezpiecznie przechowywać poufne dane, takie jak bankowe lub ubezpieczeniowe." }, "newSshNudgeTitle": { - "message": "Przyjazny dla deweloperów dostęp SSH" + "message": "Dostęp SSH przyjazny dla programistów" }, "newSshNudgeBodyOne": { - "message": "Przechowuj swoje klucze i połącz się z agentem SSH dla szybkiego, szyfrowanego uwierzytelniania.", + "message": "Przechowuj klucze i łącz się z agentem SSH w celu szybkiego, szyfrowanego uwierzytelniania.", "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, @@ -5397,20 +5420,20 @@ "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "generatorNudgeTitle": { - "message": "Szybko twórz hasła" + "message": "Szybkie tworzenie haseł" }, "generatorNudgeBodyOne": { - "message": "Łatwo twórz silne i unikalne hasła, klikając na", + "message": "Twórz silne i unikalne hasła, klikając przycisk", "description": "Two part message", "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." }, "generatorNudgeBodyTwo": { - "message": ", aby pomóc Ci zachować bezpieczeństwo Twoich danych logowania.", + "message": ", aby zapewnić bezpieczeństwo danych logowania.", "description": "Two part message", "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." }, "generatorNudgeBodyAria": { - "message": "Łatwo twórz silne i unikalne hasła, klikając na Wygeneruj Hasło, aby pomóc Ci zachować bezpieczeństwo Twoich danych logowania.", + "message": "Twórz silne i unikalne hasła, klikając przycisk Wygeneruj hasło, aby zapewnić bezpieczeństwo danych logowania.", "description": "Aria label for the body content of the generator nudge" }, "noPermissionsViewPage": { diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 4405d1c59df..3bd3e0f36de 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Você precisa verificar o seu e-mail para usar este recurso. Você pode verificar seu e-mail no cofre web." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Senha mestra atualizada" }, @@ -4244,6 +4247,26 @@ "message": "Formatos comuns", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continuar nas configurações do navegador?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index ba791785b0e..8e4ea6d6d7c 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Tem de verificar o seu e-mail para utilizar esta funcionalidade. Pode verificar o seu e-mail no cofre Web." }, + "masterPasswordSuccessfullySet": { + "message": "Palavra-passe mestra definida com sucesso" + }, "updatedMasterPassword": { "message": "Palavra-passe mestra atualizada" }, @@ -4244,6 +4247,26 @@ "message": "Formatos comuns", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "A deteção de correspondência de URI é a forma como o Bitwarden identifica sugestões de preenchimento automático.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "A \"expressão regular\" é uma opção avançada com um risco acrescido de exposição de credenciais.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Começa com\" é uma opção avançada com um risco acrescido de exposição de credenciais.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "Mais informações sobre a deteção de correspondências", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Opções avançadas", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continuar para as definições do navegador?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index b27a1cbc519..d3636beb1b8 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Trebuie să vă verificați e-mailul pentru a utiliza această caracteristică. Puteți verifica e-mailul în seiful web." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Parola principală actualizată" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index b9ffa0afbdc..031381ab09c 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Для использования этой функции необходимо подтвердить ваш email. Вы можете это сделать в веб-хранилище." }, + "masterPasswordSuccessfullySet": { + "message": "Мастер-пароль успешно установлен" + }, "updatedMasterPassword": { "message": "Мастер-пароль обновлен" }, @@ -4244,6 +4247,26 @@ "message": "Основные форматы", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "Обнаружение совпадения URI - это способ, с помощью которого Bitwarden идентифицирует предложения по автозаполнению.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Регулярное выражение\" - это расширенный вариант с повышенным риском раскрытия учетных данных.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Начинается с\" - это расширенный вариант с повышенным риском раскрытия учетных данных.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "Подробнее об обнаружении совпадений", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Расширенные настройки", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Перейти к настройкам браузера?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 9f60625ce4c..16d96c59b12 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "මෙම අංගය භාවිතා කිරීම සඳහා ඔබේ විද්යුත් තැපෑල සත්යාපනය කළ යුතුය. වෙබ් සුරක්ෂිතාගාරයේ ඔබගේ විද්යුත් තැපෑල සත්යාපනය කළ හැකිය." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "යාවත්කාලීන කරන ලද මාස්ටර් මුරපදය" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 8a10ad901e8..9abe948055f 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -1213,11 +1213,11 @@ "message": "Sekundárnym kliknutím získate prístup k vygenerovaniu hesiel a zodpovedajúcim prihláseniam pre webovú stránku. Platí pre všetky prihlásené účty." }, "defaultUriMatchDetection": { - "message": "Predvolené mapovanie", + "message": "Predvolený spôsob zisťovania zhody URI", "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { - "message": "Vyberte si predvolený spôsob mapovania, ktorý bude použitý pre prihlasovacie údaje pri využití funkcí ako je napríklad automatické vypĺňanie hesiel." + "message": "Vyberte predvolený spôsob zisťovania zhody, ktorý bude použitý pre prihlasovacie údaje pri využití funkcií, ako je napríklad automatické vypĺňanie hesiel." }, "theme": { "message": "Motív" @@ -2049,11 +2049,11 @@ "description": "A programming term, also known as 'RegEx'." }, "matchDetection": { - "message": "Spôsob mapovania", + "message": "Zisťovanie zhody", "description": "URI match detection for autofill." }, "defaultMatchDetection": { - "message": "Predvolené mapovanie", + "message": "Predvolené zisťovanie zhody", "description": "Default URI match detection for autofill." }, "toggleOptions": { @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Na použitie tejto funkcie musíte overiť svoj e-mail. Svoj e-mail môžete overiť vo webovom trezore." }, + "masterPasswordSuccessfullySet": { + "message": "Hlavné heslo bolo úspešne nastavené" + }, "updatedMasterPassword": { "message": "Hlavné heslo aktualizované" }, @@ -4244,6 +4247,26 @@ "message": "Bežné formáty", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "Zisťovanie zhody URI je spôsob, akým Bitwarden identifikuje návrhy na automatické vypĺňanie.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regulárny výraz\" je pokročilá možnosť so zvýšeným rizikom odhalenia prihlasovacích údajov.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Začína na\" je rozšírená možnosť so zvýšeným rizikom odhalenia prihlasovacích údajov.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "Viac informácií o zisťovaní zhody", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Rozšírené možnosti", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Pokračovať do nastavení prehliadača?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" @@ -4692,7 +4715,7 @@ } }, "showMatchDetection": { - "message": "Zobraziť spôsob mapovania $WEBSITE$", + "message": "Zobraziť zisťovanie zhody $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -4701,7 +4724,7 @@ } }, "hideMatchDetection": { - "message": "Skryť spôsob mapovania $WEBSITE$", + "message": "Skryť spôsob zisťovania zhody $WEBSITE$", "placeholders": { "website": { "content": "$1", diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 2d73e99023c..310e9a28d3a 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Za uporabo te funkcionalnosti morate potrditi svoj e-naslov. To lahko storite v spletnem trezorju." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Posodobi glavno geslo" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 870338e3563..935b9d4876c 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Морате да потврдите е-пошту да бисте користили ову функцију. Можете да потврдите е-пошту у веб сефу." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Главна лозинка ажурирана" }, @@ -4244,6 +4247,26 @@ "message": "Уобичајени формати", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Желите ли да наставите на подешавања претраживача?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index c4b72cc5ea1..819af390a19 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -2919,6 +2919,9 @@ "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." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Huvudlösenord uppdaterades" }, @@ -4244,6 +4247,26 @@ "message": "Vanliga format", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index b6a8d1834b4..45dc3215966 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Updated master password" }, @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index f7895c8866d..84130e006ba 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -6,39 +6,39 @@ "message": "โลโก้ Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden - จัดการรหัสผ่าน", "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": "ไม่ว่าจะอยู่ที่ไหน Bitwarden ก็สามารถปกป้องรหัสผ่าน พาสคีย์ และข้อมูลสำคัญของคุณได้อย่างง่ายดาย", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "ล็อกอิน หรือ สร้างบัญชีใหม่ เพื่อใช้งานตู้นิรภัยของคุณ" }, "inviteAccepted": { - "message": "Invitation accepted" + "message": "ตอบรับคำเชิญแล้ว" }, "createAccount": { "message": "สร้างบัญชี" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "เพิ่งเริ่มใช้ Bitwarden ใช่ไหม?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "เข้าสู่ระบบด้วยพาสคีย์" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "ใช้การลงชื่อเพียงครั้งเดียว" }, "welcomeBack": { - "message": "Welcome back" + "message": "ยินดีต้อนรับกลับมา" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "ตั้งรหัสผ่านที่รัดกุม" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" + "message": "ดำเนินการสร้างบัญชีของคุณให้เสร็จสมบูรณ์โดยการตั้งรหัสผ่าน" }, "enterpriseSingleSignOn": { "message": "Enterprise Single Sign-On" @@ -65,7 +65,7 @@ "message": "คำใบ้เกี่ยวกับรหัสผ่านหลักสามารถช่วยให้คุณนึกรหัสผ่านหลักออกได้หากลืม" }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "หากคุณลืมรหัสผ่าน ระบบสามารถส่งคำใบ้รหัสผ่านไปยังอีเมลของคุณได้ จำกัด $CURRENT$/$MAXIMUM$ ตัวอักษร", "placeholders": { "current": { "content": "$1", @@ -84,7 +84,7 @@ "message": "Master Password Hint (optional)" }, "passwordStrengthScore": { - "message": "Password strength score $SCORE$", + "message": "คะแนนความรัดกุมของรหัสผ่าน $SCORE$", "placeholders": { "score": { "content": "$1", @@ -264,7 +264,7 @@ "message": "Request password hint" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { - "message": "Enter your account email address and your password hint will be sent to you" + "message": "กรอกที่อยู่อีเมลบัญชีของคุณ แล้วระบบจะส่งคำใบ้รหัสผ่านไปให้คุณ" }, "getMasterPasswordHint": { "message": "รับคำใบ้เกี่ยวกับรหัสผ่านหลักของคุณ" @@ -668,7 +668,7 @@ "message": "ตู้เซฟของคุณถูกล็อก ยืนยันตัวตนของคุณเพื่อดำเนินการต่อ" }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "ห้องนิรภัยของคุณถูกล็อก" }, "yourAccountIsLocked": { "message": "Your account is locked" @@ -1022,7 +1022,7 @@ "message": "The \"Add Login Notification\" automatically prompts you to save new logins to your vault whenever you log into them for the first time." }, "addLoginNotificationDescAlt": { - "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." + "message": "หากไม่พบรายการในห้องนิรภัยของคุณ ระบบจะถามเพื่อเพิ่มรายการ มีผลกับทุกบัญชีที่ลงชื่อเข้าใช้" }, "showCardsInVaultViewV2": { "message": "Always show cards as Autofill suggestions on Vault view" @@ -1342,7 +1342,7 @@ "message": "ลบไฟล์แนบแล้ว" }, "newAttachment": { - "message": "Add New Attachment" + "message": "เพิ่มไฟล์แนบใหม่" }, "noAttachments": { "message": "ไม่มีไฟล์แนบ" @@ -1544,7 +1544,7 @@ "message": "FIDO2 WebAuthn" }, "webAuthnDesc": { - "message": "Use any WebAuthn compatible security key to access your account." + "message": "ใช้กุญแจความปลอดภัยที่รองรับ WebAuthn ใดก็ได้เพื่อเข้าถึงบัญชีของคุณ" }, "emailTitle": { "message": "อีเมล" @@ -1597,7 +1597,7 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Autofill suggestions" + "message": "คำแนะนำการกรอกข้อมูลอัตโนมัติ" }, "autofillSpotlightTitle": { "message": "Easily find autofill suggestions" @@ -2148,16 +2148,16 @@ "message": "ปลดล็อกด้วย PIN" }, "setYourPinTitle": { - "message": "Set PIN" + "message": "ตั้ง PIN" }, "setYourPinButton": { - "message": "Set PIN" + "message": "ตั้ง PIN" }, "setYourPinCode": { "message": "ตั้ง PIN เพื่อใช้ปลดล็อก Bitwarden ทั้งนี้ หากคุณล็อกเอาต์ออกจากแอปโดยสมบูรณ์จะเป็นการลบการตั้งค่า PIN ของคุณด้วย" }, "setPinCode": { - "message": "You can use this PIN to unlock Bitwarden. Your PIN will be reset if you ever fully log out of the application." + "message": "ตั้ง PIN เพื่อใช้ปลดล็อก Bitwarden และหากคุณล็อกเอาต์ออกจากแอปโดยสมบูรณ์จะเป็นการลบการตั้งค่า PIN ของคุณ" }, "pinRequired": { "message": "ต้องระบุ PIN" @@ -2172,7 +2172,7 @@ "message": "ปลดล็อกด้วยไบโอเมตริก" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "เข้าสู่ระบบด้วยรหัสผ่านหลัก" }, "awaitDesktop": { "message": "Awaiting confirmation from desktop" @@ -2184,7 +2184,7 @@ "message": "ล็อคด้วยรหัสผ่านหลักเมื่อรีสตาร์ทเบราว์เซอร์" }, "lockWithMasterPassOnRestart1": { - "message": "Require master password on browser restart" + "message": "กำหนดให้ป้อนรหัสผ่านหลักเมื่อรีสตาร์ทเบราว์เซอร์" }, "selectOneCollection": { "message": "คุณต้องเลือกอย่างน้อยหนึ่งคอลเลกชัน" @@ -2902,7 +2902,7 @@ "message": "Hide your email address from viewers." }, "passwordPrompt": { - "message": "Master password re-prompt" + "message": "การยืนยันให้ป้อนรหัสผ่านหลักอีกครั้ง" }, "passwordConfirmation": { "message": "Master password confirmation" @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Updated master password" }, @@ -3416,7 +3419,7 @@ "message": "Fingerprint phrase" }, "fingerprintMatchInfo": { - "message": "Please make sure your vault is unlocked and the Fingerprint phrase matches on the other device." + "message": "โปรดตรวจสอบให้แน่ใจว่าห้องนิรภัยของคุณปลดล็อกอยู่ และลายนิ้วมือตรงกันบนอุปกรณ์อื่น" }, "resendNotification": { "message": "Resend notification" @@ -3639,10 +3642,10 @@ "message": "Organization is not trusted" }, "emergencyAccessTrustWarning": { - "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + "message": "เพื่อความปลอดภัยของบัญชีของคุณ โปรดยืนยันว่าคุณได้ให้สิทธิ์การเข้าถึงในกรณีฉุกเฉินแก่ผู้ใช้นี้ และลายนิ้วมือของผู้ใช้ตรงกับที่แสดงในบัญชีของพวกเขาเท่านั้น" }, "orgTrustWarning": { - "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + "message": "เพื่อความปลอดภัยของบัญชีของคุณ ให้ดำเนินการต่อเมื่อคุณเป็นสมาชิกขององค์กรนี้, ได้เปิดใช้งานการกู้คืนบัญชี, และลายนิ้วมือที่แสดงด้านล่างตรงกับลายนิ้วมือขององค์กรเท่านั้น" }, "orgTrustWarning1": { "message": "This organization has an Enterprise policy that will enroll you in account recovery. Enrollment will allow organization administrators to change your password. Only proceed if you recognize this organization and the fingerprint phrase displayed below matches the organization's fingerprint." @@ -3920,7 +3923,7 @@ "message": "Verification required for this action. Set a PIN to continue." }, "setPin": { - "message": "Set PIN" + "message": "ตั้ง PIN" }, "verifyWithBiometrics": { "message": "Verify with biometrics" @@ -4017,10 +4020,10 @@ "message": "Select the import file" }, "chooseFile": { - "message": "Choose File" + "message": "เลือกไฟล์" }, "noFileChosen": { - "message": "No file chosen" + "message": "ไม่มีไฟล์ที่เลือก" }, "orCopyPasteFileContents": { "message": "or copy/paste the import file contents" @@ -4051,7 +4054,7 @@ "message": "Passkey" }, "accessing": { - "message": "Accessing" + "message": "กำลังเข้าถึง" }, "loggedInExclamation": { "message": "Logged in!" @@ -4205,7 +4208,7 @@ "message": "Available accounts" }, "accountLimitReached": { - "message": "Account limit reached. Log out of an account to add another." + "message": "ถึงขีดจำกัดของบัญชีแล้ว กรุณาออกจากระบบบัญชีอื่นเพื่อเพิ่มบัญชีใหม่" }, "active": { "message": "active" @@ -4244,6 +4247,26 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Continue to browser settings?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" @@ -4460,7 +4483,7 @@ "message": "Admin Console" }, "accountSecurity": { - "message": "Account security" + "message": "ความปลอดภัยของบัญชี" }, "notifications": { "message": "Notifications" @@ -4533,10 +4556,10 @@ "message": "Additional information" }, "itemHistory": { - "message": "Item history" + "message": "ประวัติการแก้ไขรายการ" }, "lastEdited": { - "message": "Last edited" + "message": "แก้ไขล่าสุดเมื่อ" }, "ownerYou": { "message": "Owner: You" @@ -4548,13 +4571,13 @@ "message": "Copy Successful" }, "upload": { - "message": "Upload" + "message": "อัปโหลด" }, "addAttachment": { - "message": "Add attachment" + "message": "เพิ่มไฟล์แนบ" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "ขนาดไฟล์สูงสุด คือ 500 MB" }, "deleteAttachmentName": { "message": "Delete attachment $NAME$", @@ -4657,7 +4680,7 @@ "message": "Authenticator key" }, "autofillOptions": { - "message": "Autofill options" + "message": "ตัวเลือกในการป้อนอัตโนมัติ" }, "websiteUri": { "message": "Website (URI)" @@ -4737,7 +4760,7 @@ "message": "Show animations" }, "addAccount": { - "message": "Add account" + "message": "เพิ่มบัญชี" }, "loading": { "message": "Loading" @@ -4779,7 +4802,7 @@ } }, "addField": { - "message": "Add field" + "message": "เพิ่มฟิลด์" }, "add": { "message": "Add" @@ -4791,7 +4814,7 @@ "message": "Field label" }, "textHelpText": { - "message": "Use text fields for data like security questions" + "message": "ใช้ช่องข้อความสำหรับเก็บข้อมูล เช่น คำถามเพื่อความปลอดภัย" }, "hiddenHelpText": { "message": "Use hidden fields for sensitive data like a password" @@ -4827,7 +4850,7 @@ } }, "fieldAdded": { - "message": "$LABEL$ added", + "message": "เพิ่ม $LABEL$ แล้ว", "placeholders": { "label": { "content": "$1", @@ -4958,7 +4981,7 @@ "message": "Text Sends" }, "accountActions": { - "message": "Account actions" + "message": "การจัดการบัญชี" }, "showNumberOfAutofillSuggestions": { "message": "Show number of login autofill suggestions on extension icon" @@ -5069,7 +5092,7 @@ "message": "You can customize your unlock and timeout settings to more quickly access your vault." }, "unlockPinSet": { - "message": "Unlock PIN set" + "message": "ตั้งค่า PIN สำหรับปลดล็อกแล้ว" }, "unlockWithBiometricSet": { "message": "Unlock with biometrics set" @@ -5309,7 +5332,7 @@ "message": "Quick and easy login" }, "quickLoginBody": { - "message": "Set up biometric unlock and autofill to log into your accounts without typing a single letter." + "message": "ตั้งค่าการปลดล็อกด้วยไบโอเมตริกซ์และการกรอกข้อมูลอัตโนมัติ เพื่อลงชื่อเข้าใช้บัญชีของคุณโดยไม่ต้องพิมพ์แม้แต่ตัวอักษรเดียว" }, "secureUser": { "message": "Level up your logins" diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index aae4fdd2486..f523df3c8d4 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Bu özelliği kullanmak için e-postanızı doğrulamanız gerekir. E-postanızı web kasasında doğrulayabilirsiniz." }, + "masterPasswordSuccessfullySet": { + "message": "Ana parola başarıyla ayarlandı" + }, "updatedMasterPassword": { "message": "Ana parola güncellendi" }, @@ -4244,6 +4247,26 @@ "message": "Sık kullanılan biçimler", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Gelişmiş seçenekler", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Tarayıcı ayarlarına gidilsin mi?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index ab4fe87a2be..e7d5d0dbdc8 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "Для використання цієї функції необхідно підтвердити електронну пошту. Ви можете виконати підтвердження у вебсховищі." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Головний пароль оновлено" }, @@ -4244,6 +4247,26 @@ "message": "Поширені формати", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Відкрити налаштування браузера?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index d02658f9e26..b979680cd81 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -2919,6 +2919,9 @@ "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." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Đã cập nhật mật khẩu chính" }, @@ -4244,6 +4247,26 @@ "message": "Định dạng chung", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "Tiếp tục tới Cài đặt trình duyệt?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index fe70f8abe57..023d0a682d5 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "您必须验证电子邮箱才能使用此功能。您可以在网页密码库中验证您的电子邮箱。" }, + "masterPasswordSuccessfullySet": { + "message": "主密码设置成功" + }, "updatedMasterPassword": { "message": "已更新主密码" }, @@ -4244,6 +4247,26 @@ "message": "常规格式", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "前往浏览器设置吗?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index e50117419b2..07ad7b207da 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "您必須驗證您的電子郵件才能使用此功能。您可以在網頁密碼庫裡驗證您的電子郵件。" }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "已更新主密碼" }, @@ -4244,6 +4247,26 @@ "message": "常見格式", "description": "Label indicating the most common import formats" }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" + }, + "uriAdvancedOption": { + "message": "Advanced options", + "description": "Advanced option placeholder for uri option component" + }, "confirmContinueToBrowserSettingsTitle": { "message": "繼續前往瀏覽器設定?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" diff --git a/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.spec.ts b/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.spec.ts index 28db10b35fa..9202a5a3839 100644 --- a/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.spec.ts +++ b/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.spec.ts @@ -44,6 +44,40 @@ describe("OverlayNotificationsContentService", () => { expect(bodyAppendChildSpy).not.toHaveBeenCalled(); }); + it("applies correct styles when notificationRefreshFlag is true", async () => { + overlayNotificationsContentService["notificationRefreshFlag"] = true; + + sendMockExtensionMessage({ + command: "openNotificationBar", + data: { + type: "change", + typeData: mock(), + }, + }); + await flushPromises(); + + const barElement = overlayNotificationsContentService["notificationBarElement"]!; + expect(barElement.style.height).toBe("400px"); + expect(barElement.style.right).toBe("0px"); + }); + + it("applies correct styles when notificationRefreshFlag is false", async () => { + overlayNotificationsContentService["notificationRefreshFlag"] = false; + + sendMockExtensionMessage({ + command: "openNotificationBar", + data: { + type: "change", + typeData: mock(), + }, + }); + await flushPromises(); + + const barElement = overlayNotificationsContentService["notificationBarElement"]!; + expect(barElement.style.height).toBe("82px"); + expect(barElement.style.right).toBe("10px"); + }); + it("closes the notification bar if the notification bar type has changed", async () => { overlayNotificationsContentService["currentNotificationBarType"] = "add"; const closeNotificationBarSpy = jest.spyOn( diff --git a/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts b/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts index ee005852a42..01f8237581d 100644 --- a/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts +++ b/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts @@ -21,24 +21,32 @@ export class OverlayNotificationsContentService private notificationBarIframeElement: HTMLIFrameElement | null = null; private currentNotificationBarType: NotificationType | null = null; private notificationRefreshFlag: boolean = false; - private notificationBarElementStyles: Partial = { - height: "82px", - width: "430px", - maxWidth: "calc(100% - 20px)", - minHeight: "initial", - top: "10px", - right: "10px", - padding: "0", - position: "fixed", - zIndex: "2147483647", - visibility: "visible", - borderRadius: "4px", - border: "none", - backgroundColor: "transparent", - overflow: "hidden", - transition: "box-shadow 0.15s ease", - transitionDelay: "0.15s", - }; + private getNotificationBarStyles(): Partial { + const styles: Partial = { + height: "400px", + width: "430px", + maxWidth: "calc(100% - 20px)", + minHeight: "initial", + top: "10px", + right: "0px", + padding: "0", + position: "fixed", + zIndex: "2147483647", + visibility: "visible", + borderRadius: "4px", + border: "none", + backgroundColor: "transparent", + overflow: "hidden", + transition: "box-shadow 0.15s ease", + transitionDelay: "0.15s", + }; + + if (!this.notificationRefreshFlag) { + styles.height = "82px"; + styles.right = "10px"; + } + return styles; + } private notificationBarIframeElementStyles: Partial = { width: "100%", height: "100%", @@ -60,7 +68,6 @@ export class OverlayNotificationsContentService void sendExtensionMessage("checkNotificationQueue"); void sendExtensionMessage("notificationRefreshFlagValue").then((notificationRefreshFlag) => { this.notificationRefreshFlag = !!notificationRefreshFlag; - this.setNotificationRefreshBarHeight(); }); } @@ -233,32 +240,12 @@ export class OverlayNotificationsContentService this.notificationBarElement = globalThis.document.createElement("div"); this.notificationBarElement.id = "bit-notification-bar"; - setElementStyles(this.notificationBarElement, this.notificationBarElementStyles, true); - this.setNotificationRefreshBarHeight(); + setElementStyles(this.notificationBarElement, this.getNotificationBarStyles(), true); this.notificationBarElement.appendChild(this.notificationBarIframeElement); } } - /** - * Sets the height of the notification bar based on the value of `notificationRefreshFlag`. - * If the flag is `true`, the bar is expanded to 400px and aligned right. - * If the flag is `false`, `null`, or `undefined`, it defaults to height of 82px. - * Skips if the notification bar element has not yet been created. - * - */ - private setNotificationRefreshBarHeight() { - const isNotificationV3 = !!this.notificationRefreshFlag; - - if (!this.notificationBarElement) { - return; - } - - if (isNotificationV3) { - setElementStyles(this.notificationBarElement, { height: "400px", right: "0" }, true); - } - } - /** * Sets up the message listener for the initialization of the notification bar. * This will send the initialization data to the notification bar iframe. diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 9f6529643c4..aa80222c672 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "Bitwarden", - "version": "2025.6.0", + "version": "2025.6.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", @@ -56,7 +56,8 @@ "unlimitedStorage", "webNavigation", "webRequest", - "webRequestBlocking" + "webRequestBlocking", + "notifications" ], "__safari__permissions": [ "", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index bf5c4e439b9..6d38a5880d5 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "Bitwarden", - "version": "2025.6.0", + "version": "2025.6.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/platform/popup/layout/popup-layout.mdx b/apps/browser/src/platform/popup/layout/popup-layout.mdx index 017ee20b344..a2725350a8f 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.mdx +++ b/apps/browser/src/platform/popup/layout/popup-layout.mdx @@ -54,6 +54,9 @@ page looks nice when the extension is popped out. `false`. - `loadingText` - Custom text to be applied to the loading element for screenreaders only. Defaults to "Loading". +- `disablePadding` + - When `true`, disables the padding of the scrollable region inside of `main`. You will need to + add your own padding to the element you place inside of this area. Basic usage example: @@ -169,6 +172,22 @@ When the browser extension is popped out, the "popout" button should not be pass +## With Virtual Scroll + +If you are using a virtual scrolling container inside of the popup page, you'll want to apply the +`bitScrollLayout` directive to the `cdk-virtual-scroll-viewport` element. This tells the virtual +scroll viewport to use the popup page's scroll layout div as the scrolling container. + +See the code in the example below. + + + +### Known Virtual Scroll Issues + +See [Virtual Scrolling](?path=/docs/documentation-virtual-scrolling--docs#known-footgun) for more +information about how to structure virtual scrolling containers with layout components and avoid a +known issue with template construction. + # Other stories ## Centered Content diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts index aecbaf673dc..894ab13dd19 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -1,5 +1,4 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore +import { ScrollingModule } from "@angular/cdk/scrolling"; import { CommonModule } from "@angular/common"; import { Component, importProvidersFrom } from "@angular/core"; import { RouterModule } from "@angular/router"; @@ -20,6 +19,7 @@ import { NoItemsModule, SearchModule, SectionComponent, + ScrollLayoutDirective, } from "@bitwarden/components"; import { PopupRouterCacheService } from "../view-cache/popup-router-cache.service"; @@ -39,6 +39,17 @@ import { PopupTabNavigationComponent } from "./popup-tab-navigation.component"; }) class ExtensionContainerComponent {} +@Component({ + selector: "extension-popped-container", + template: ` +
+ +
+ `, + standalone: true, +}) +class ExtensionPoppedContainerComponent {} + @Component({ selector: "vault-placeholder", template: /*html*/ ` @@ -295,6 +306,7 @@ export default { decorators: [ moduleMetadata({ imports: [ + ScrollLayoutDirective, PopupTabNavigationComponent, PopupHeaderComponent, PopupPageComponent, @@ -302,6 +314,7 @@ export default { CommonModule, RouterModule, ExtensionContainerComponent, + ExtensionPoppedContainerComponent, MockBannerComponent, MockSearchComponent, MockVaultSubpageComponent, @@ -312,6 +325,11 @@ export default { MockVaultPagePoppedComponent, NoItemsModule, VaultComponent, + ScrollingModule, + ItemModule, + SectionComponent, + IconButtonModule, + BadgeModule, ], providers: [ { @@ -495,7 +513,21 @@ export const CompactMode: Story = { const compact = canvasEl.querySelector( `#${containerId} [data-testid=popup-layout-scroll-region]`, ); + + if (!compact) { + // eslint-disable-next-line + console.error(`#${containerId} [data-testid=popup-layout-scroll-region] not found`); + return; + } + const label = canvasEl.querySelector(`#${containerId} .example-label`); + + if (!label) { + // eslint-disable-next-line + console.error(`#${containerId} .example-label not found`); + return; + } + const percentVisible = 100 - Math.round((100 * (compact.scrollHeight - compact.clientHeight)) / compact.scrollHeight); @@ -510,9 +542,9 @@ export const PoppedOut: Story = { render: (args) => ({ props: args, template: /* HTML */ ` -
+ -
+ `, }), }; @@ -560,10 +592,9 @@ export const TransparentHeader: Story = { template: /* HTML */ ` - 🤠 Custom Content - + + 🤠 Custom Content + @@ -608,3 +639,56 @@ export const WidthOptions: Story = { `, }), }; + +export const WithVirtualScrollChild: Story = { + render: (args) => ({ + props: { ...args, data: Array.from(Array(20).keys()) }, + template: /* HTML */ ` + + + + + + @defer (on immediate) { + + + + + + + + + + + + + + + + + + + + } + + + + `, + }), +}; diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.html b/apps/browser/src/platform/popup/layout/popup-page.component.html index 2313b942a38..b53ef6e97eb 100644 --- a/apps/browser/src/platform/popup/layout/popup-page.component.html +++ b/apps/browser/src/platform/popup/layout/popup-page.component.html @@ -1,29 +1,39 @@
+
+
-
- -
+
  • - + { const isTrialPaymentOptional = await firstValueFrom(this.trialPaymentOptional$); - if (isTrialPaymentOptional) { + /** Only skip payment if the flag is on AND trialLength > 0 */ + if (isTrialPaymentOptional && this.trialLength > 0) { await this.createOrganizationOnTrial(); } else { await this.conditionallyCreateOrganization(); @@ -333,6 +337,18 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy { return this.productTier; } + readonly showBillingStep$ = combineLatest([ + this.trialPaymentOptional$, + this.allowTrialLengthZero$, + ]).pipe( + map(([trialPaymentOptional, allowTrialLengthZero]) => { + return ( + (!trialPaymentOptional && !this.isSecretsManagerFree) || + (trialPaymentOptional && allowTrialLengthZero && this.trialLength === 0) + ); + }), + ); + /** Create an organization unless the trial is for secrets manager */ async conditionallyCreateOrganization(): Promise { if (!this.isSecretsManagerFree) { diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 46435981a5e..b8baa762e91 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -10,6 +10,7 @@ import { OrganizationUserApiService, CollectionService, } from "@bitwarden/admin-console/common"; +import { SetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction"; import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { CLIENT_TYPE, @@ -61,6 +62,7 @@ import { VaultTimeoutStringType, } from "@bitwarden/common/key-management/vault-timeout"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService, Urls, @@ -117,6 +119,7 @@ import { WebLoginDecryptionOptionsService, WebTwoFactorAuthDuoComponentService, LinkSsoService, + WebSetInitialPasswordService, } from "../auth"; import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service"; import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service"; @@ -254,6 +257,7 @@ const safeProviders: SafeProvider[] = [ LogService, PolicyService, AccountService, + ConfigService, ], }), safeProvider({ @@ -283,6 +287,24 @@ const safeProviders: SafeProvider[] = [ InternalUserDecryptionOptionsServiceAbstraction, ], }), + safeProvider({ + provide: SetInitialPasswordService, + useClass: WebSetInitialPasswordService, + deps: [ + ApiService, + EncryptService, + I18nServiceAbstraction, + KdfConfigService, + KeyServiceAbstraction, + MasterPasswordApiService, + InternalMasterPasswordServiceAbstraction, + OrganizationApiServiceAbstraction, + OrganizationUserApiService, + InternalUserDecryptionOptionsServiceAbstraction, + AcceptOrganizationInviteService, + RouterService, + ], + }), safeProvider({ provide: AppIdService, useClass: DefaultAppIdService, diff --git a/apps/web/src/app/core/event.service.ts b/apps/web/src/app/core/event.service.ts index d6e5c8eba5f..14c87181f62 100644 --- a/apps/web/src/app/core/event.service.ts +++ b/apps/web/src/app/core/event.service.ts @@ -467,8 +467,20 @@ export class EventService { break; // Secrets Manager case EventType.Secret_Retrieved: - msg = this.i18nService.t("accessedSecret", this.formatSecretId(ev)); - humanReadableMsg = this.i18nService.t("accessedSecret", this.getShortId(ev.secretId)); + msg = this.i18nService.t("accessedSecretWithId", this.formatSecretId(ev)); + humanReadableMsg = this.i18nService.t("accessedSecretWithId", this.getShortId(ev.secretId)); + break; + case EventType.Secret_Created: + msg = this.i18nService.t("createdSecretWithId", this.formatSecretId(ev)); + humanReadableMsg = this.i18nService.t("createdSecretWithId", this.getShortId(ev.secretId)); + break; + case EventType.Secret_Deleted: + msg = this.i18nService.t("deletedSecretWithId", this.formatSecretId(ev)); + humanReadableMsg = this.i18nService.t("deletedSecretWithId", this.getShortId(ev.secretId)); + break; + case EventType.Secret_Edited: + msg = this.i18nService.t("editedSecretWithId", this.formatSecretId(ev)); + humanReadableMsg = this.i18nService.t("editedSecretWithId", this.getShortId(ev.secretId)); break; default: break; diff --git a/apps/web/src/app/core/web-file-download.service.ts b/apps/web/src/app/core/web-file-download.service.ts index ad034702a55..3421203737a 100644 --- a/apps/web/src/app/core/web-file-download.service.ts +++ b/apps/web/src/app/core/web-file-download.service.ts @@ -12,7 +12,7 @@ export class WebFileDownloadService implements FileDownloadService { download(request: FileDownloadRequest): void { const builder = new FileDownloadBuilder(request); const a = window.document.createElement("a"); - if (!this.platformUtilsService.isSafari()) { + if (!this.platformUtilsService.supportsFileDownloads()) { a.rel = "noreferrer"; a.target = "_blank"; } diff --git a/apps/web/src/app/core/web-platform-utils.service.spec.ts b/apps/web/src/app/core/web-platform-utils.service.spec.ts index 3b5cb96b718..6dba3fda782 100644 --- a/apps/web/src/app/core/web-platform-utils.service.spec.ts +++ b/apps/web/src/app/core/web-platform-utils.service.spec.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { DeviceType } from "@bitwarden/common/enums"; + import { WebPlatformUtilsService } from "./web-platform-utils.service"; describe("Web Platform Utils Service", () => { @@ -114,4 +116,91 @@ describe("Web Platform Utils Service", () => { expect(result).toBe("2022.10.2"); }); }); + describe("getDevice", () => { + const originalUserAgent = navigator.userAgent; + + const setUserAgent = (userAgent: string) => { + Object.defineProperty(navigator, "userAgent", { + value: userAgent, + configurable: true, + }); + }; + + const setWindowProperties = (props?: Record) => { + if (!props) { + return; + } + Object.keys(props).forEach((key) => { + Object.defineProperty(window, key, { + value: props[key], + configurable: true, + }); + }); + }; + + afterEach(() => { + // Reset to original after each test + setUserAgent(originalUserAgent); + }); + + const testData: { + userAgent: string; + expectedDevice: DeviceType; + windowProps?: Record; + }[] = [ + { + // DuckDuckGo macoOS browser v1.13 + userAgent: + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3.1 Safari/605.1.15 Ddg/18.3.1", + expectedDevice: DeviceType.DuckDuckGoBrowser, + }, + // DuckDuckGo Windows browser v0.109.7, which does not present the Ddg suffix and is therefore detected as Edge + { + userAgent: + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0", + expectedDevice: DeviceType.EdgeBrowser, + }, + { + userAgent: + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + expectedDevice: DeviceType.ChromeBrowser, + windowProps: { chrome: {} }, // set window.chrome = {} to simulate Chrome + }, + { + userAgent: + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0", + expectedDevice: DeviceType.FirefoxBrowser, + }, + { + userAgent: + "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15", + expectedDevice: DeviceType.SafariBrowser, + }, + { + userAgent: + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edg/120.0.0.0 Chrome/120.0.0.0 Safari/537.36", + expectedDevice: DeviceType.EdgeBrowser, + }, + { + userAgent: + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.65 Safari/537.36 OPR/95.0.4635.46", + expectedDevice: DeviceType.OperaBrowser, + }, + { + userAgent: + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.57 Safari/537.36 Vivaldi/6.5.3206.48", + expectedDevice: DeviceType.VivaldiBrowser, + }, + ]; + + test.each(testData)( + "returns $expectedDevice for User-Agent: $userAgent", + ({ userAgent, expectedDevice, windowProps }) => { + setUserAgent(userAgent); + setWindowProperties(windowProps); + const result = webPlatformUtilsService.getDevice(); + expect(result).toBe(expectedDevice); + }, + ); + }); }); diff --git a/apps/web/src/app/core/web-platform-utils.service.ts b/apps/web/src/app/core/web-platform-utils.service.ts index 3df2a7d895b..c3d1f5e3c1a 100644 --- a/apps/web/src/app/core/web-platform-utils.service.ts +++ b/apps/web/src/app/core/web-platform-utils.service.ts @@ -34,6 +34,13 @@ export class WebPlatformUtilsService implements PlatformUtilsService { this.browserCache = DeviceType.EdgeBrowser; } else if (navigator.userAgent.indexOf(" Vivaldi/") !== -1) { this.browserCache = DeviceType.VivaldiBrowser; + } else if ( + // We are only detecting DuckDuckGo browser on macOS currently, as + // it is not presenting the Ddg suffix on Windows. DuckDuckGo users + // on Windows will be detected as Edge. + navigator.userAgent.indexOf("Ddg") !== -1 + ) { + this.browserCache = DeviceType.DuckDuckGoBrowser; } else if ( navigator.userAgent.indexOf(" Safari/") !== -1 && navigator.userAgent.indexOf("Chrome") === -1 @@ -83,6 +90,10 @@ export class WebPlatformUtilsService implements PlatformUtilsService { return this.getDevice() === DeviceType.SafariBrowser; } + isWebKit(): boolean { + return true; + } + isMacAppStore(): boolean { return false; } @@ -120,6 +131,15 @@ export class WebPlatformUtilsService implements PlatformUtilsService { return true; } + supportsAutofill(): boolean { + return false; + } + + // Safari support for blob downloads is inconsistent and requires workarounds + supportsFileDownloads(): boolean { + return !(this.getDevice() === DeviceType.SafariBrowser); + } + showToast( type: "error" | "success" | "warning" | "info", title: string, diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html index 08195d95bf9..f8ebfa60451 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html @@ -16,43 +16,45 @@ > {{ "moreFromBitwarden" | i18n }} - - - -
    - {{ more.otherProductOverrides?.name ?? more.name }} -
    - {{ more.otherProductOverrides.supportingText }} + - - - - - diff --git a/apps/web/src/app/layouts/user-layout.component.ts b/apps/web/src/app/layouts/user-layout.component.ts index cd07d625281..db4c181cd0f 100644 --- a/apps/web/src/app/layouts/user-layout.component.ts +++ b/apps/web/src/app/layouts/user-layout.component.ts @@ -9,11 +9,10 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { SyncService } from "@bitwarden/common/platform/sync"; -import { IconModule } from "@bitwarden/components"; +import { IconModule, PasswordManagerLogo } from "@bitwarden/components"; import { BillingFreeFamiliesNavItemComponent } from "../billing/shared/billing-free-families-nav-item.component"; -import { PasswordManagerLogo } from "./password-manager-logo"; import { WebLayoutModule } from "./web-layout.module"; @Component({ diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 783fe6ada0a..31b9ca26e70 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -10,6 +10,8 @@ import { unauthGuardFn, activeAuthGuard, } from "@bitwarden/angular/auth/guards"; +import { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component"; +import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { PasswordHintComponent, RegistrationFinishComponent, @@ -36,6 +38,7 @@ import { NewDeviceVerificationComponent, DeviceVerificationIcon, } from "@bitwarden/auth/angular"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, Icons } from "@bitwarden/components"; import { LockComponent } from "@bitwarden/key-management-ui"; import { VaultIcons } from "@bitwarden/vault"; @@ -78,6 +81,7 @@ import { AccessComponent, SendAccessExplainerComponent } from "./tools/send/send import { SendComponent } from "./tools/send/send.component"; import { BrowserExtensionPromptInstallComponent } from "./vault/components/browser-extension-prompt/browser-extension-prompt-install.component"; import { BrowserExtensionPromptComponent } from "./vault/components/browser-extension-prompt/browser-extension-prompt.component"; +import { SetupExtensionComponent } from "./vault/components/setup-extension/setup-extension.component"; import { VaultModule } from "./vault/individual-vault/vault.module"; const routes: Routes = [ @@ -305,6 +309,14 @@ const routes: Routes = [ }, ], }, + { + path: "set-initial-password", + canActivate: [canAccessFeature(FeatureFlag.PM16117_SetInitialPasswordRefactor), authGuard], + component: SetInitialPasswordComponent, + data: { + maxWidth: "lg", + } satisfies AnonLayoutWrapperData, + }, { path: "set-password-jit", component: SetPasswordJitComponent, @@ -347,7 +359,6 @@ const routes: Routes = [ pageSubtitle: { key: "singleSignOnEnterOrgIdentifierText", }, - titleAreaMaxWidth: "md", pageIcon: SsoKeyIcon, } satisfies RouteDataProperties & AnonLayoutWrapperData, children: [ @@ -381,7 +392,6 @@ const routes: Routes = [ pageTitle: { key: "verifyYourIdentity", }, - titleAreaMaxWidth: "md", } satisfies RouteDataProperties & AnonLayoutWrapperData, }, { @@ -570,6 +580,20 @@ const routes: Routes = [ }, ], }, + { + path: "setup-extension", + data: { + hideCardWrapper: true, + hideIcon: true, + maxWidth: "3xl", + } satisfies AnonLayoutWrapperData, + children: [ + { + path: "", + component: SetupExtensionComponent, + }, + ], + }, ], }, { diff --git a/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.html b/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.html new file mode 100644 index 00000000000..df1786e227e --- /dev/null +++ b/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.html @@ -0,0 +1,25 @@ + +
    + +
    + +
    + {{ "cannotAutofillPasswordsWithoutExtensionTitle" | i18n }} +
    +
    {{ "cannotAutofillPasswordsWithoutExtensionDesc" | i18n }}
    +
    + + + {{ "getTheExtension" | i18n }} + + + {{ "skipToWebApp" | i18n }} + + +
    diff --git a/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.spec.ts b/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.spec.ts new file mode 100644 index 00000000000..d34dba737dd --- /dev/null +++ b/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.spec.ts @@ -0,0 +1,42 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { provideNoopAnimations } from "@angular/platform-browser/animations"; +import { RouterModule } from "@angular/router"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +import { AddExtensionLaterDialogComponent } from "./add-extension-later-dialog.component"; + +describe("AddExtensionLaterDialogComponent", () => { + let fixture: ComponentFixture; + const getDevice = jest.fn().mockReturnValue(null); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AddExtensionLaterDialogComponent, RouterModule.forRoot([])], + providers: [ + provideNoopAnimations(), + { provide: PlatformUtilsService, useValue: { getDevice } }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(AddExtensionLaterDialogComponent); + fixture.detectChanges(); + }); + + it("renders the 'Get the Extension' link with correct href", () => { + const link = fixture.debugElement.queryAll(By.css("a[bitButton]"))[0]; + + expect(link.nativeElement.getAttribute("href")).toBe( + "https://bitwarden.com/download/#downloads-web-browser", + ); + }); + + it("renders the 'Skip to Web App' link with correct routerLink", () => { + const skipLink = fixture.debugElement.queryAll(By.css("a[bitButton]"))[1]; + + expect(skipLink.attributes.href).toBe("/vault"); + }); +}); diff --git a/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.ts b/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.ts new file mode 100644 index 00000000000..3324cb8b1b0 --- /dev/null +++ b/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.ts @@ -0,0 +1,23 @@ +import { Component, inject, OnInit } from "@angular/core"; +import { RouterModule } from "@angular/router"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { getWebStoreUrl } from "@bitwarden/common/vault/utils/get-web-store-url"; +import { ButtonComponent, DialogModule, TypographyModule } from "@bitwarden/components"; + +@Component({ + selector: "vault-add-extension-later-dialog", + templateUrl: "./add-extension-later-dialog.component.html", + imports: [DialogModule, JslibModule, TypographyModule, ButtonComponent, RouterModule], +}) +export class AddExtensionLaterDialogComponent implements OnInit { + private platformUtilsService = inject(PlatformUtilsService); + + /** Download Url for the extension based on the browser */ + protected webStoreUrl: string = ""; + + ngOnInit(): void { + this.webStoreUrl = getWebStoreUrl(this.platformUtilsService.getDevice()); + } +} diff --git a/apps/web/src/app/vault/components/setup-extension/add-extension-videos.component.html b/apps/web/src/app/vault/components/setup-extension/add-extension-videos.component.html new file mode 100644 index 00000000000..3764f7d828f --- /dev/null +++ b/apps/web/src/app/vault/components/setup-extension/add-extension-videos.component.html @@ -0,0 +1,82 @@ + + + + + + + + + + + + + +
    +
    +
    + +
    + +
    +
    + +
    + +
    +
    + +
    +
    diff --git a/apps/web/src/app/vault/components/setup-extension/add-extension-videos.component.spec.ts b/apps/web/src/app/vault/components/setup-extension/add-extension-videos.component.spec.ts new file mode 100644 index 00000000000..9f39a3edcac --- /dev/null +++ b/apps/web/src/app/vault/components/setup-extension/add-extension-videos.component.spec.ts @@ -0,0 +1,170 @@ +import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { provideNoopAnimations } from "@angular/platform-browser/animations"; +import { RouterModule } from "@angular/router"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { AddExtensionVideosComponent } from "./add-extension-videos.component"; + +describe("AddExtensionVideosComponent", () => { + let fixture: ComponentFixture; + let component: AddExtensionVideosComponent; + + // Mock HTMLMediaElement load to stop the video file from being loaded + Object.defineProperty(HTMLMediaElement.prototype, "load", { + value: jest.fn(), + writable: true, + }); + + const play = jest.fn(() => Promise.resolve()); + HTMLMediaElement.prototype.play = play; + + beforeEach(async () => { + window.matchMedia = jest.fn().mockReturnValue(false); + play.mockClear(); + + await TestBed.configureTestingModule({ + imports: [AddExtensionVideosComponent, RouterModule.forRoot([])], + providers: [ + provideNoopAnimations(), + { provide: I18nService, useValue: { t: (key: string) => key } }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(AddExtensionVideosComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe("loading pulse", () => { + it("shows loading spinner when all videos are not loaded", () => { + const loadingSpinners = fixture.debugElement.queryAll(By.css("[data-testid='video-pulse']")); + expect(loadingSpinners.length).toBe(3); + }); + + it("shows all pulses until all videos are loaded", () => { + let loadingSpinners = fixture.debugElement.queryAll(By.css("[data-testid='video-pulse']")); + expect(loadingSpinners.length).toBe(3); + + // Simulate two video loaded + component["videoElements"].get(0)?.nativeElement.dispatchEvent(new Event("loadeddata")); + component["videoElements"].get(1)?.nativeElement.dispatchEvent(new Event("loadeddata")); + + loadingSpinners = fixture.debugElement.queryAll(By.css("[data-testid='video-pulse']")); + + expect(component["numberOfLoadedVideos"]).toBe(2); + expect(loadingSpinners.length).toBe(3); + }); + }); + + describe("window resizing", () => { + beforeEach(() => { + component["numberOfLoadedVideos"] = 3; + fixture.detectChanges(); + }); + + it("shows all videos when window is resized to desktop viewport", fakeAsync(() => { + component["variant"] = "mobile"; + Object.defineProperty(component["document"].documentElement, "clientWidth", { + configurable: true, + value: 1000, + }); + + window.dispatchEvent(new Event("resize")); + + fixture.detectChanges(); + tick(50); + + expect( + Array.from(component["videoElements"]).every( + (video) => video.nativeElement.parentElement?.style.opacity === "1", + ), + ).toBe(true); + })); + + it("shows only the playing video when window is resized to mobile viewport", fakeAsync(() => { + component["variant"] = "desktop"; + // readonly property needs redefining + Object.defineProperty(component["document"].documentElement, "clientWidth", { + value: 500, + }); + + const video1 = component["videoElements"].get(1); + Object.defineProperty(video1!.nativeElement, "paused", { + value: false, + }); + + window.dispatchEvent(new Event("resize")); + + fixture.detectChanges(); + tick(50); + + expect(component["videoElements"].get(0)?.nativeElement.parentElement?.style.opacity).toBe( + "0", + ); + expect(component["videoElements"].get(1)?.nativeElement.parentElement?.style.opacity).toBe( + "1", + ); + expect(component["videoElements"].get(2)?.nativeElement.parentElement?.style.opacity).toBe( + "0", + ); + })); + }); + + describe("video sequence", () => { + let firstVideo: HTMLVideoElement; + let secondVideo: HTMLVideoElement; + let thirdVideo: HTMLVideoElement; + + beforeEach(() => { + component["numberOfLoadedVideos"] = 2; + component["onVideoLoad"](); + + firstVideo = component["videoElements"].get(0)!.nativeElement; + secondVideo = component["videoElements"].get(1)!.nativeElement; + thirdVideo = component["videoElements"].get(2)!.nativeElement; + }); + + it("starts the video sequence when all videos are loaded", fakeAsync(() => { + tick(); + + expect(firstVideo.play).toHaveBeenCalled(); + })); + + it("plays videos in sequence", fakeAsync(() => { + tick(); // let first video play + + play.mockClear(); + firstVideo.onended!(new Event("ended")); // trigger next video + + tick(); + + expect(secondVideo.play).toHaveBeenCalledTimes(1); + + play.mockClear(); + secondVideo.onended!(new Event("ended")); // trigger next video + + tick(); + + expect(thirdVideo.play).toHaveBeenCalledTimes(1); + })); + + it("doesn't play videos again when the user prefers no motion", fakeAsync(() => { + component["prefersReducedMotion"] = true; + + tick(); + firstVideo.onended!(new Event("ended")); + tick(); + secondVideo.onended!(new Event("ended")); + tick(); + + play.mockClear(); + + thirdVideo.onended!(new Event("ended")); // trigger first video again + + tick(); + expect(play).toHaveBeenCalledTimes(0); + })); + }); +}); diff --git a/apps/web/src/app/vault/components/setup-extension/add-extension-videos.component.ts b/apps/web/src/app/vault/components/setup-extension/add-extension-videos.component.ts new file mode 100644 index 00000000000..2420414fc88 --- /dev/null +++ b/apps/web/src/app/vault/components/setup-extension/add-extension-videos.component.ts @@ -0,0 +1,146 @@ +import { CommonModule, DOCUMENT } from "@angular/common"; +import { Component, ViewChildren, QueryList, ElementRef, inject } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { debounceTime, fromEvent } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; + +@Component({ + selector: "vault-add-extension-videos", + templateUrl: "./add-extension-videos.component.html", + imports: [CommonModule, JslibModule], +}) +export class AddExtensionVideosComponent { + @ViewChildren("video", { read: ElementRef }) protected videoElements!: QueryList< + ElementRef + >; + + private document = inject(DOCUMENT); + + /** Current viewport size */ + protected variant: "mobile" | "desktop" = "desktop"; + + /** Number of videos that have loaded and are ready to play */ + protected numberOfLoadedVideos = 0; + + /** True when the user prefers reduced motion */ + protected prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + + /** Returns true when all videos are loaded */ + get allVideosLoaded(): boolean { + return this.numberOfLoadedVideos >= 3; + } + + constructor() { + fromEvent(window, "resize") + .pipe(takeUntilDestroyed(), debounceTime(25)) + .subscribe(() => this.onResize()); + } + + /** Resets the video states based on the viewport width changes */ + onResize(): void { + const oldVariant = this.variant; + this.variant = this.document.documentElement.clientWidth < 768 ? "mobile" : "desktop"; + + // When the viewport changes from desktop to mobile, hide all videos except the one that is playing. + if (this.variant !== oldVariant && this.variant === "mobile") { + this.videoElements.forEach((video) => { + if (video.nativeElement.paused) { + this.hideElement(video.nativeElement.parentElement!); + } else { + this.showElement(video.nativeElement.parentElement!); + } + }); + } + + // When the viewport changes from mobile to desktop, show all videos. + if (this.variant !== oldVariant && this.variant === "desktop") { + this.videoElements.forEach((video) => { + this.showElement(video.nativeElement.parentElement!); + }); + } + } + + /** + * Increment the number of loaded videos. + * When all videos are loaded, start the first one. + */ + protected onVideoLoad() { + this.numberOfLoadedVideos = this.numberOfLoadedVideos + 1; + + if (this.allVideosLoaded) { + void this.startVideoSequence(0); + } + } + + /** Recursive method to start the video sequence. */ + private async startVideoSequence(i: number): Promise { + let index = i; + const endOfVideos = index >= this.videoElements.length; + + // When the user prefers reduced motion, don't play the videos more than once + if (endOfVideos && this.prefersReducedMotion) { + return; + } + + // When the last of the videos has played, loop back to the start + if (endOfVideos) { + this.videoElements.forEach((video) => { + // Reset all videos to the start + video.nativeElement.currentTime = 0; + }); + + // Loop back to the first video + index = 0; + } + + const video = this.videoElements.toArray()[index].nativeElement; + video.onended = () => { + void this.startVideoSequence(index + 1); + }; + + this.mobileTransitionIn(index); + + // Set muted via JavaScript, browsers are respecting autoplay consistently over just the HTML attribute + video.muted = true; + await video.play(); + } + + /** For mobile viewports, fades the current video out and the next video in. */ + private mobileTransitionIn(index: number): void { + // When the viewport is above the tablet breakpoint, all videos are shown at once. + // No transition is needed. + if (this.isAboveTabletBreakpoint()) { + return; + } + + const currentParent = this.videoElements.toArray()[index].nativeElement.parentElement!; + const previousIndex = index === 0 ? this.videoElements.length - 1 : index - 1; + + const previousParent = this.videoElements.toArray()[previousIndex].nativeElement.parentElement!; + + // Fade out the previous video + this.hideElement(previousParent, true); + + // Fade in the current video + this.showElement(currentParent, true); + } + + /** Returns true when the viewport width is 768px or above. */ + private isAboveTabletBreakpoint(): boolean { + const width = this.document.documentElement.clientWidth; + return width >= 768; + } + + /** Visually hides the given element. */ + private hideElement(element: HTMLElement, transition = false): void { + element.style.transition = transition ? "opacity 0.5s linear" : ""; + element.style.opacity = "0"; + } + + /** Visually shows the given element. */ + private showElement(element: HTMLElement, transition = false): void { + element.style.transition = transition ? "opacity 0.5s linear" : ""; + element.style.opacity = "1"; + } +} diff --git a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.html b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.html new file mode 100644 index 00000000000..3b9ec19fd34 --- /dev/null +++ b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.html @@ -0,0 +1,56 @@ + + +
    +

    + {{ "setupExtensionPageTitle" | i18n }} +

    +

    + {{ "setupExtensionPageDescription" | i18n }} +

    + +
    + + {{ "getTheExtension" | i18n }} + + +
    +
    + +
    + +

    {{ "bitwardenExtensionInstalled" | i18n }}

    +
    +

    {{ "openExtensionToAutofill" | i18n }}

    + + + {{ "skipToWebApp" | i18n }} + +
    +

    + {{ "gettingStartedWithBitwardenPart1" | i18n }} + + {{ "gettingStartedWithBitwardenPart2" | i18n }} + + {{ "and" | i18n }} + + {{ "gettingStartedWithBitwardenPart3" | i18n }} + +

    +
    diff --git a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts new file mode 100644 index 00000000000..752e2c8d4a6 --- /dev/null +++ b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts @@ -0,0 +1,125 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { Router, RouterModule } from "@angular/router"; +import { BehaviorSubject } from "rxjs"; + +import { DeviceType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; + +import { WebBrowserInteractionService } from "../../services/web-browser-interaction.service"; + +import { SetupExtensionComponent } from "./setup-extension.component"; + +describe("SetupExtensionComponent", () => { + let fixture: ComponentFixture; + let component: SetupExtensionComponent; + + const getFeatureFlag = jest.fn().mockResolvedValue(false); + const navigate = jest.fn().mockResolvedValue(true); + const openExtension = jest.fn().mockResolvedValue(true); + const extensionInstalled$ = new BehaviorSubject(null); + + beforeEach(async () => { + navigate.mockClear(); + openExtension.mockClear(); + getFeatureFlag.mockClear().mockResolvedValue(true); + window.matchMedia = jest.fn().mockReturnValue(false); + + await TestBed.configureTestingModule({ + imports: [SetupExtensionComponent, RouterModule.forRoot([])], + providers: [ + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: ConfigService, useValue: { getFeatureFlag } }, + { provide: WebBrowserInteractionService, useValue: { extensionInstalled$, openExtension } }, + { provide: PlatformUtilsService, useValue: { getDevice: () => DeviceType.UnknownBrowser } }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(SetupExtensionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + const router = TestBed.inject(Router); + router.navigate = navigate; + }); + + it("initially shows the loading spinner", () => { + const spinner = fixture.debugElement.query(By.css("i")); + + expect(spinner.nativeElement.title).toBe("loading"); + }); + + it("sets webStoreUrl", () => { + expect(component["webStoreUrl"]).toBe("https://bitwarden.com/download/#downloads-web-browser"); + }); + + describe("initialization", () => { + it("redirects to the vault if the feature flag is disabled", async () => { + Utils.isMobileBrowser = false; + getFeatureFlag.mockResolvedValue(false); + navigate.mockClear(); + + await component.ngOnInit(); + + expect(navigate).toHaveBeenCalledWith(["/vault"]); + }); + + it("redirects to the vault if the user is on a mobile browser", async () => { + Utils.isMobileBrowser = true; + getFeatureFlag.mockResolvedValue(true); + navigate.mockClear(); + + await component.ngOnInit(); + + expect(navigate).toHaveBeenCalledWith(["/vault"]); + }); + + it("does not redirect the user", async () => { + Utils.isMobileBrowser = false; + getFeatureFlag.mockResolvedValue(true); + navigate.mockClear(); + + await component.ngOnInit(); + + expect(getFeatureFlag).toHaveBeenCalledWith(FeatureFlag.PM19315EndUserActivationMvp); + expect(navigate).not.toHaveBeenCalled(); + }); + }); + + describe("extensionInstalled$", () => { + it("redirects the user to the vault when the first emitted value is true", () => { + extensionInstalled$.next(true); + + expect(navigate).toHaveBeenCalledWith(["/vault"]); + }); + + describe("success state", () => { + beforeEach(() => { + // avoid initial redirect + extensionInstalled$.next(false); + + fixture.detectChanges(); + + extensionInstalled$.next(true); + fixture.detectChanges(); + }); + + it("shows link to the vault", () => { + const successLink = fixture.debugElement.query(By.css("a")); + + expect(successLink.nativeElement.href).toContain("/vault"); + }); + + it("shows open extension button", () => { + const openExtensionButton = fixture.debugElement.query(By.css("button")); + + openExtensionButton.triggerEventHandler("click"); + + expect(openExtension).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts new file mode 100644 index 00000000000..9ee8e189627 --- /dev/null +++ b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts @@ -0,0 +1,141 @@ +import { DOCUMENT, NgIf } from "@angular/common"; +import { Component, DestroyRef, inject, OnDestroy, OnInit } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { Router, RouterModule } from "@angular/router"; +import { pairwise, startWith } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values"; +import { getWebStoreUrl } from "@bitwarden/common/vault/utils/get-web-store-url"; +import { + ButtonComponent, + DialogRef, + DialogService, + IconModule, + LinkModule, +} from "@bitwarden/components"; +import { VaultIcons } from "@bitwarden/vault"; + +import { WebBrowserInteractionService } from "../../services/web-browser-interaction.service"; + +import { AddExtensionLaterDialogComponent } from "./add-extension-later-dialog.component"; +import { AddExtensionVideosComponent } from "./add-extension-videos.component"; + +const SetupExtensionState = { + Loading: "loading", + NeedsExtension: "needs-extension", + Success: "success", +} as const; + +type SetupExtensionState = UnionOfValues; + +@Component({ + selector: "vault-setup-extension", + templateUrl: "./setup-extension.component.html", + imports: [ + NgIf, + JslibModule, + ButtonComponent, + LinkModule, + IconModule, + RouterModule, + AddExtensionVideosComponent, + ], +}) +export class SetupExtensionComponent implements OnInit, OnDestroy { + private webBrowserExtensionInteractionService = inject(WebBrowserInteractionService); + private configService = inject(ConfigService); + private router = inject(Router); + private destroyRef = inject(DestroyRef); + private platformUtilsService = inject(PlatformUtilsService); + private dialogService = inject(DialogService); + private document = inject(DOCUMENT); + + protected SetupExtensionState = SetupExtensionState; + protected PartyIcon = VaultIcons.Party; + + /** The current state of the setup extension component. */ + protected state: SetupExtensionState = SetupExtensionState.Loading; + + /** Download Url for the extension based on the browser */ + protected webStoreUrl: string = ""; + + /** Reference to the add it later dialog */ + protected dialogRef: DialogRef | null = null; + private viewportContent: string | null = null; + + async ngOnInit() { + // It is not be uncommon for users to hit this page from smaller viewports. + // There are global styles that set a min-width for the page which cause it to render poorly. + // Remove them here. + // https://github.com/bitwarden/clients/blob/main/apps/web/src/scss/base.scss#L6 + this.document.body.style.minWidth = "auto"; + + const viewportMeta = this.document.querySelector('meta[name="viewport"]'); + + // Save the current viewport content to reset it when the component is destroyed + this.viewportContent = viewportMeta?.getAttribute("content") ?? null; + viewportMeta?.setAttribute("content", "width=device-width, initial-scale=1.0"); + + await this.conditionallyRedirectUser(); + + this.webStoreUrl = getWebStoreUrl(this.platformUtilsService.getDevice()); + + this.webBrowserExtensionInteractionService.extensionInstalled$ + .pipe(takeUntilDestroyed(this.destroyRef), startWith(null), pairwise()) + .subscribe(([previousState, currentState]) => { + // Initial state transitioned to extension installed, redirect the user + if (previousState === null && currentState) { + void this.router.navigate(["/vault"]); + } + + // Extension was not installed and now it is, show success state + if (previousState === false && currentState) { + this.dialogRef?.close(); + this.state = SetupExtensionState.Success; + } + + // Extension is not installed + if (currentState === false) { + this.state = SetupExtensionState.NeedsExtension; + } + }); + } + + ngOnDestroy(): void { + // Reset the body min-width when the component is destroyed + this.document.body.style.minWidth = ""; + + if (this.viewportContent !== null) { + this.document + .querySelector('meta[name="viewport"]') + ?.setAttribute("content", this.viewportContent); + } + } + + /** Conditionally redirects the user to the vault upon landing on the page. */ + async conditionallyRedirectUser() { + const isFeatureEnabled = await this.configService.getFeatureFlag( + FeatureFlag.PM19315EndUserActivationMvp, + ); + const isMobile = Utils.isMobileBrowser; + + if (!isFeatureEnabled || isMobile) { + await this.router.navigate(["/vault"]); + } + } + + /** Opens the add extension later dialog */ + addItLater() { + this.dialogRef = this.dialogService.open(AddExtensionLaterDialogComponent); + } + + /** Opens the browser extension */ + openExtension() { + void this.webBrowserExtensionInteractionService.openExtension(); + } +} diff --git a/apps/web/src/app/vault/services/web-browser-interaction.service.spec.ts b/apps/web/src/app/vault/services/web-browser-interaction.service.spec.ts index 68a9ca6d099..fef5d45e8c3 100644 --- a/apps/web/src/app/vault/services/web-browser-interaction.service.spec.ts +++ b/apps/web/src/app/vault/services/web-browser-interaction.service.spec.ts @@ -61,6 +61,7 @@ describe("WebBrowserInteractionService", () => { tick(1500); expect(results[0]).toBe(false); + tick(2500); // then emit `HasBwInstalled` dispatchEvent(VaultMessages.HasBwInstalled); tick(); diff --git a/apps/web/src/app/vault/services/web-browser-interaction.service.ts b/apps/web/src/app/vault/services/web-browser-interaction.service.ts index 46c566140e4..f1005ef6dc9 100644 --- a/apps/web/src/app/vault/services/web-browser-interaction.service.ts +++ b/apps/web/src/app/vault/services/web-browser-interaction.service.ts @@ -1,6 +1,21 @@ import { DestroyRef, inject, Injectable } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { concatWith, filter, fromEvent, map, Observable, race, take, tap, timer } from "rxjs"; +import { + concat, + filter, + fromEvent, + interval, + map, + Observable, + of, + race, + shareReplay, + switchMap, + take, + takeWhile, + tap, + timer, +} from "rxjs"; import { ExtensionPageUrls } from "@bitwarden/common/vault/enums"; import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum"; @@ -22,13 +37,22 @@ export class WebBrowserInteractionService { ); /** Emits the installation status of the extension. */ - extensionInstalled$ = this.checkForExtension().pipe( - concatWith( - this.messages$.pipe( - filter((event) => event.data.command === VaultMessages.HasBwInstalled), - map(() => true), - ), - ), + extensionInstalled$: Observable = this.checkForExtension().pipe( + switchMap((installed) => { + if (installed) { + return of(true); + } + + return concat( + of(false), + interval(2500).pipe( + switchMap(() => this.checkForExtension()), + takeWhile((installed) => !installed, true), + filter((installed) => installed), + ), + ); + }), + shareReplay({ bufferSize: 1, refCount: true }), ); /** Attempts to open the extension, rejects if the extension is not installed or it fails to open. */ diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 5c9b02e5287..c5a7a64ee5c 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -6065,6 +6065,9 @@ "add": { "message": "Add" }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Master password saved" }, @@ -8234,8 +8237,44 @@ "errorReadingImportFile": { "message": "An error occurred when trying to read the import file" }, + "accessedSecretWithId": { + "message": "Accessed a secret with identifier: $SECRET_ID$", + "placeholders": { + "secret_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "accessedSecret": { - "message": "Accessed secret $SECRET_ID$.", + "message": "Accessed secret $SECRET_ID$.", + "placeholders": { + "secret_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "editedSecretWithId": { + "message": "Edited a secret with identifier: $SECRET_ID$", + "placeholders": { + "secret_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "deletedSecretWithId": { + "message": "Deleted a secret with identifier: $SECRET_ID$", + "placeholders": { + "secret_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "createdSecretWithId": { + "message": "Created a new secret with identifier: $SECRET_ID$", "placeholders": { "secret_id": { "content": "$1", @@ -8868,6 +8907,22 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "uriMatchDefaultStrategyHint": { + "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + }, + "regExAdvancedOptionWarning": { + "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" + }, + "startsWithAdvancedOptionWarning": { + "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" + }, + "uriMatchWarningDialogLink": { + "message": "More about match detection", + "description": "Link to match detection docs on warning dialog for advance match strategy" }, "maintainYourSubscription": { "message": "To maintain your subscription for $ORG$, ", @@ -10659,6 +10714,51 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "setupExtensionPageTitle": { + "message": "Autofill your passwords securely with one click" + }, + "setupExtensionPageDescription": { + "message": "Get the Bitwarden browser extension and start autofilling today" + }, + "getTheExtension": { + "message": "Get the extension" + }, + "addItLater": { + "message": "Add it later" + }, + "cannotAutofillPasswordsWithoutExtensionTitle": { + "message": "You can't autofill passwords without the browser extension" + }, + "cannotAutofillPasswordsWithoutExtensionDesc": { + "message": "Are you sure you don't want to add the extension now?" + }, + "skipToWebApp": { + "message": "Skip to web app" + }, + "bitwardenExtensionInstalled": { + "message": "Bitwarden extension installed!" + }, + "openExtensionToAutofill": { + "message": "Open the extension to log in and start autofilling." + }, + "openBitwardenExtension": { + "message": "Open Bitwarden extension" + }, + "gettingStartedWithBitwardenPart1": { + "message": "For tips on getting started with Bitwarden visit the", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'For tips on getting started with Bitwarden visit the Learning Center and Help Center'" + }, + "gettingStartedWithBitwardenPart2": { + "message": "Learning Center", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'For tips on getting started with Bitwarden visit the Learning Center and Help Center'" + }, + "gettingStartedWithBitwardenPart3": { + "message": "Help Center", + "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'For tips on getting started with Bitwarden visit the Learning Center and Help Center'" + }, + "setupExtensionContentAlt": { + "message": "With the Bitwarden browser extension you can easily create new logins, access your saved logins directly from your browser toolbar, and sign in to accounts quickly using Bitwarden autofill." + }, "restart": { "message": "Restart" }, diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js index 04a68b16c00..a28052b98e3 100644 --- a/apps/web/webpack.config.js +++ b/apps/web/webpack.config.js @@ -268,6 +268,9 @@ const devServer = https://www.paypalobjects.com https://q.stripe.com https://haveibeenpwned.com + ;media-src + 'self' + https://assets.bitwarden.com ;child-src 'self' https://js.stripe.com diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations-routing.module.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations-routing.module.ts index f63140a8b23..35659d05dce 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations-routing.module.ts @@ -79,6 +79,7 @@ const routes: Routes = [ }, { path: "access-intelligence", + canActivate: [organizationPermissionsGuard((org) => org.canAccessReports)], loadChildren: () => import("../../dirt/access-intelligence/access-intelligence.module").then( (m) => m.AccessIntelligenceModule, diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence-routing.module.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence-routing.module.ts index 6df0f01bc8b..2e3c53d8d9f 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence-routing.module.ts @@ -9,7 +9,9 @@ const routes: Routes = [ { path: "", pathMatch: "full", redirectTo: "risk-insights" }, { path: "risk-insights", - canActivate: [organizationPermissionsGuard((org) => org.useRiskInsights)], + canActivate: [ + organizationPermissionsGuard((org) => org.useRiskInsights && org.canAccessReports), + ], component: RiskInsightsComponent, data: { titleId: "RiskInsights", diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health-members-uri.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health-members-uri.component.html deleted file mode 100644 index 936d92d8701..00000000000 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health-members-uri.component.html +++ /dev/null @@ -1,51 +0,0 @@ - -

    {{ "passwordsReportDesc" | i18n }}

    -
    - - {{ "loading" | i18n }} -
    -
    - - - {{ "application" | i18n }} - {{ "weakness" | i18n }} - {{ "timesReused" | i18n }} - {{ "timesExposed" | i18n }} - {{ "totalMembers" | i18n }} - - - - - {{ row.hostURI }} - - - - - {{ passwordStrengthMap.get(row.id)[0] | i18n }} - - - - - {{ "reusedXTimes" | i18n: passwordUseMap.get(row.login.password) }} - - - - - {{ "exposedXTimes" | i18n: exposedPasswordMap.get(row.id) }} - - - - {{ totalMembersMap.get(row.id) || 0 }} - - - -
    -
    diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health-members-uri.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health-members-uri.component.spec.ts deleted file mode 100644 index e827f884ead..00000000000 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health-members-uri.component.spec.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { ActivatedRoute, convertToParamMap } from "@angular/router"; -import { mock, MockProxy } from "jest-mock-extended"; -import { of } from "rxjs"; - -import { - MemberCipherDetailsApiService, - PasswordHealthService, -} from "@bitwarden/bit-common/dirt/reports/risk-insights"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AuditService } from "@bitwarden/common/abstractions/audit.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { mockAccountServiceWith } from "@bitwarden/common/spec"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; -import { UserId } from "@bitwarden/common/types/guid"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { TableModule } from "@bitwarden/components"; -import { LooseComponentsModule } from "@bitwarden/web-vault/app/shared"; -import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; - -import { PasswordHealthMembersURIComponent } from "./password-health-members-uri.component"; - -describe("PasswordHealthMembersUriComponent", () => { - let component: PasswordHealthMembersURIComponent; - let fixture: ComponentFixture; - let cipherServiceMock: MockProxy; - const passwordHealthServiceMock = mock(); - const userId = Utils.newGuid() as UserId; - - const activeRouteParams = convertToParamMap({ organizationId: "orgId" }); - - beforeEach(async () => { - cipherServiceMock = mock(); - await TestBed.configureTestingModule({ - imports: [PasswordHealthMembersURIComponent, PipesModule, TableModule, LooseComponentsModule], - providers: [ - { provide: CipherService, useValue: cipherServiceMock }, - { provide: I18nService, useValue: mock() }, - { provide: AuditService, useValue: mock() }, - { provide: OrganizationService, useValue: mock() }, - { provide: AccountService, useValue: mockAccountServiceWith(userId) }, - { - provide: PasswordStrengthServiceAbstraction, - useValue: mock(), - }, - { provide: PasswordHealthService, useValue: passwordHealthServiceMock }, - { - provide: ActivatedRoute, - useValue: { - paramMap: of(activeRouteParams), - url: of([]), - }, - }, - { - provide: MemberCipherDetailsApiService, - useValue: mock(), - }, - { - provide: ApiService, - useValue: mock(), - }, - ], - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(PasswordHealthMembersURIComponent); - component = fixture.componentInstance; - }); - - it("should initialize component", () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health-members-uri.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health-members-uri.component.ts deleted file mode 100644 index a4e8dd0ded8..00000000000 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health-members-uri.component.ts +++ /dev/null @@ -1,107 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { CommonModule } from "@angular/common"; -import { Component, DestroyRef, inject, OnInit } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { ActivatedRoute } from "@angular/router"; -import { map } from "rxjs"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { - MemberCipherDetailsApiService, - PasswordHealthService, -} from "@bitwarden/bit-common/dirt/reports/risk-insights"; -import { AuditService } from "@bitwarden/common/abstractions/audit.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { - BadgeModule, - BadgeVariant, - ContainerComponent, - TableDataSource, - TableModule, -} from "@bitwarden/components"; -import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; -import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; - -@Component({ - selector: "tools-password-health-members-uri", - templateUrl: "password-health-members-uri.component.html", - imports: [ - BadgeModule, - CommonModule, - ContainerComponent, - PipesModule, - JslibModule, - HeaderModule, - TableModule, - ], - providers: [PasswordHealthService, MemberCipherDetailsApiService], -}) -export class PasswordHealthMembersURIComponent implements OnInit { - passwordStrengthMap = new Map(); - - weakPasswordCiphers: CipherView[] = []; - - passwordUseMap = new Map(); - - exposedPasswordMap = new Map(); - - totalMembersMap = new Map(); - - dataSource = new TableDataSource(); - - reportCiphers: (CipherView & { hostURI: string })[] = []; - reportCipherURIs: string[] = []; - - organization: Organization; - - loading = true; - - private destroyRef = inject(DestroyRef); - - constructor( - protected cipherService: CipherService, - protected passwordStrengthService: PasswordStrengthServiceAbstraction, - protected organizationService: OrganizationService, - protected auditService: AuditService, - protected i18nService: I18nService, - protected activatedRoute: ActivatedRoute, - protected memberCipherDetailsApiService: MemberCipherDetailsApiService, - ) {} - - ngOnInit() { - this.activatedRoute.paramMap - .pipe( - takeUntilDestroyed(this.destroyRef), - map(async (params) => { - const organizationId = params.get("organizationId"); - await this.setCiphers(organizationId); - }), - ) - .subscribe(); - } - - async setCiphers(organizationId: string) { - const passwordHealthService = new PasswordHealthService( - this.passwordStrengthService, - this.auditService, - this.cipherService, - this.memberCipherDetailsApiService, - organizationId, - ); - - await passwordHealthService.generateReport(); - - this.dataSource.data = passwordHealthService.groupCiphersByLoginUri(); - this.exposedPasswordMap = passwordHealthService.exposedPasswordMap; - this.passwordStrengthMap = passwordHealthService.passwordStrengthMap; - this.passwordUseMap = passwordHealthService.passwordUseMap; - this.totalMembersMap = passwordHealthService.totalMembersMap; - this.loading = false; - } -} diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health-members.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health-members.component.html deleted file mode 100644 index 5c980f75a81..00000000000 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health-members.component.html +++ /dev/null @@ -1,60 +0,0 @@ -

    {{ "passwordsReportDesc" | i18n }}

    -
    - - {{ "loading" | i18n }} -
    -
    - - - - {{ "name" | i18n }} - {{ "weakness" | i18n }} - {{ "timesReused" | i18n }} - {{ "timesExposed" | i18n }} - {{ "totalMembers" | i18n }} - - - - - - - - {{ row.name }} - -
    - {{ row.subTitle }} - - - - {{ passwordStrengthMap.get(row.id)[0] | i18n }} - - - - - {{ "reusedXTimes" | i18n: passwordUseMap.get(row.login.password) }} - - - - - {{ "exposedXTimes" | i18n: exposedPasswordMap.get(row.id) }} - - - - {{ totalMembersMap.get(row.id) || 0 }} - -
    -
    -
    diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health-members.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health-members.component.ts deleted file mode 100644 index 8cad1f2f8ce..00000000000 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health-members.component.ts +++ /dev/null @@ -1,127 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, DestroyRef, inject, OnInit } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { FormControl, FormsModule } from "@angular/forms"; -import { ActivatedRoute } from "@angular/router"; -import { debounceTime, map } from "rxjs"; - -import { - MemberCipherDetailsApiService, - PasswordHealthService, -} from "@bitwarden/bit-common/dirt/reports/risk-insights"; -import { AuditService } from "@bitwarden/common/abstractions/audit.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { - BadgeVariant, - SearchModule, - TableDataSource, - TableModule, - ToastService, -} from "@bitwarden/components"; -import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; -import { SharedModule } from "@bitwarden/web-vault/app/shared"; -import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; - -@Component({ - selector: "tools-password-health-members", - templateUrl: "password-health-members.component.html", - imports: [PipesModule, HeaderModule, SearchModule, FormsModule, SharedModule, TableModule], - providers: [PasswordHealthService, MemberCipherDetailsApiService], -}) -export class PasswordHealthMembersComponent implements OnInit { - passwordStrengthMap = new Map(); - - passwordUseMap = new Map(); - - exposedPasswordMap = new Map(); - - totalMembersMap = new Map(); - - dataSource = new TableDataSource(); - - loading = true; - - selectedIds: Set = new Set(); - - protected searchControl = new FormControl("", { nonNullable: true }); - - private destroyRef = inject(DestroyRef); - - constructor( - protected cipherService: CipherService, - protected passwordStrengthService: PasswordStrengthServiceAbstraction, - protected auditService: AuditService, - protected i18nService: I18nService, - protected activatedRoute: ActivatedRoute, - protected toastService: ToastService, - protected memberCipherDetailsApiService: MemberCipherDetailsApiService, - ) { - this.searchControl.valueChanges - .pipe(debounceTime(200), takeUntilDestroyed()) - .subscribe((v) => (this.dataSource.filter = v)); - } - - ngOnInit() { - this.activatedRoute.paramMap - .pipe( - takeUntilDestroyed(this.destroyRef), - map(async (params) => { - const organizationId = params.get("organizationId"); - await this.setCiphers(organizationId); - }), - ) - .subscribe(); - } - - async setCiphers(organizationId: string) { - const passwordHealthService = new PasswordHealthService( - this.passwordStrengthService, - this.auditService, - this.cipherService, - this.memberCipherDetailsApiService, - organizationId, - ); - - await passwordHealthService.generateReport(); - - this.dataSource.data = passwordHealthService.reportCiphers; - - this.exposedPasswordMap = passwordHealthService.exposedPasswordMap; - this.passwordStrengthMap = passwordHealthService.passwordStrengthMap; - this.passwordUseMap = passwordHealthService.passwordUseMap; - this.totalMembersMap = passwordHealthService.totalMembersMap; - this.loading = false; - } - - markAppsAsCritical = async () => { - // TODO: Send to API once implemented - return new Promise((resolve) => { - setTimeout(() => { - this.selectedIds.clear(); - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("appsMarkedAsCritical"), - }); - resolve(true); - }, 1000); - }); - }; - - trackByFunction(_: number, item: CipherView) { - return item.id; - } - - onCheckboxChange(id: number, event: Event) { - const isChecked = (event.target as HTMLInputElement).checked; - if (isChecked) { - this.selectedIds.add(id); - } else { - this.selectedIds.delete(id); - } - } -} diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health.component.html deleted file mode 100644 index b798a75ab3a..00000000000 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health.component.html +++ /dev/null @@ -1,53 +0,0 @@ - -

    {{ "passwordsReportDesc" | i18n }}

    -
    - - {{ "loading" | i18n }} -
    -
    - - - - {{ "name" | i18n }} - {{ "weakness" | i18n }} - {{ "timesReused" | i18n }} - {{ "timesExposed" | i18n }} - - - - - - - - {{ row.name }} - -
    - {{ row.subTitle }} - - - - {{ row.weakPasswordDetail?.detailValue.label | i18n }} - - - - - {{ "reusedXTimes" | i18n: passwordUseMap.get(row.login.password) }} - - - - - {{ "exposedXTimes" | i18n: row.exposedPasswordDetail?.exposedXTimes }} - - -
    -
    -
    -
    diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health.component.spec.ts deleted file mode 100644 index f7c821f123a..00000000000 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health.component.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { ActivatedRoute, convertToParamMap } from "@angular/router"; -import { mock } from "jest-mock-extended"; -import { of } from "rxjs"; - -import { RiskInsightsReportService } from "@bitwarden/bit-common/dirt/reports/risk-insights"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { TableModule } from "@bitwarden/components"; -import { LooseComponentsModule } from "@bitwarden/web-vault/app/shared"; -import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; - -import { PasswordHealthComponent } from "./password-health.component"; - -describe("PasswordHealthComponent", () => { - let component: PasswordHealthComponent; - let fixture: ComponentFixture; - const activeRouteParams = convertToParamMap({ organizationId: "orgId" }); - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [PasswordHealthComponent, PipesModule, TableModule, LooseComponentsModule], - declarations: [], - providers: [ - { provide: RiskInsightsReportService, useValue: mock() }, - { provide: I18nService, useValue: mock() }, - { - provide: ActivatedRoute, - useValue: { - paramMap: of(activeRouteParams), - url: of([]), - }, - }, - ], - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(PasswordHealthComponent); - component = fixture.componentInstance; - - fixture.detectChanges(); - }); - - it("should initialize component", () => { - expect(component).toBeTruthy(); - }); - - it("should call generateReport on init", () => {}); -}); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health.component.ts deleted file mode 100644 index 16c783c3f4f..00000000000 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/password-health.component.ts +++ /dev/null @@ -1,69 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { CommonModule } from "@angular/common"; -import { Component, DestroyRef, inject, OnInit } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { ActivatedRoute } from "@angular/router"; -import { firstValueFrom, map } from "rxjs"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { RiskInsightsReportService } from "@bitwarden/bit-common/dirt/reports/risk-insights"; -import { CipherHealthReportDetail } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { - BadgeModule, - ContainerComponent, - TableDataSource, - TableModule, -} from "@bitwarden/components"; -import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; -import { OrganizationBadgeModule } from "@bitwarden/web-vault/app/vault/individual-vault/organization-badge/organization-badge.module"; -import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; - -@Component({ - selector: "tools-password-health", - templateUrl: "password-health.component.html", - imports: [ - BadgeModule, - OrganizationBadgeModule, - CommonModule, - ContainerComponent, - PipesModule, - JslibModule, - HeaderModule, - TableModule, - ], -}) -export class PasswordHealthComponent implements OnInit { - passwordUseMap = new Map(); - dataSource = new TableDataSource(); - - loading = true; - - private destroyRef = inject(DestroyRef); - - constructor( - protected riskInsightsReportService: RiskInsightsReportService, - protected i18nService: I18nService, - protected activatedRoute: ActivatedRoute, - ) {} - - ngOnInit() { - this.activatedRoute.paramMap - .pipe( - takeUntilDestroyed(this.destroyRef), - map(async (params) => { - const organizationId = params.get("organizationId"); - await this.setCiphers(organizationId); - }), - ) - .subscribe(); - } - - async setCiphers(organizationId: string) { - this.dataSource.data = await firstValueFrom( - this.riskInsightsReportService.generateRawDataReport$(organizationId), - ); - this.loading = false; - } -} diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html index c9408b806ff..627db269097 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html @@ -45,15 +45,6 @@ - - - - - - - - - = new Observable(); - showDebugTabs: boolean = false; appsCount: number = 0; criticalAppsCount: number = 0; @@ -97,8 +89,6 @@ export class RiskInsightsComponent implements OnInit { } async ngOnInit() { - this.showDebugTabs = devFlagEnabled("showRiskInsightsDebug"); - this.route.paramMap .pipe( takeUntilDestroyed(this.destroyRef), diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts index 1eef528b639..24da000f213 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts @@ -22,7 +22,7 @@ import { import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { SecretsManagerLogo } from "@bitwarden/web-vault/app/layouts/secrets-manager-logo"; +import { SecretsManagerLogo } from "@bitwarden/components"; import { OrganizationCounts } from "../models/view/counts.view"; import { ProjectService } from "../projects/project.service"; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.ts index a7ee818a01f..18ac0a80454 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.ts @@ -180,22 +180,4 @@ export class SecretsListComponent implements OnDestroy { i18nService.t("valueCopied", i18nService.t("uuid")), ); } - - /** - * TODO: Remove in favor of updating `PlatformUtilsService.copyToClipboard` - */ - private static copyToClipboardAsync( - text: Promise, - platformUtilsService: PlatformUtilsService, - ) { - if (platformUtilsService.isSafari()) { - return navigator.clipboard.write([ - new ClipboardItem({ - ["text/plain"]: text, - }), - ]); - } - - return text.then((t) => platformUtilsService.copyToClipboard(t)); - } } diff --git a/libs/angular/src/auth/guards/auth.guard.ts b/libs/angular/src/auth/guards/auth.guard.ts index f99a91fda34..7b8c21fef62 100644 --- a/libs/angular/src/auth/guards/auth.guard.ts +++ b/libs/angular/src/auth/guards/auth.guard.ts @@ -39,7 +39,32 @@ export const authGuard: CanActivateFn = async ( return false; } - if (authStatus === AuthenticationStatus.Locked) { + const userId = (await firstValueFrom(accountService.activeAccount$)).id; + const forceSetPasswordReason = await firstValueFrom( + masterPasswordService.forceSetPasswordReason$(userId), + ); + + const isSetInitialPasswordFlagOn = await configService.getFeatureFlag( + FeatureFlag.PM16117_SetInitialPasswordRefactor, + ); + const isChangePasswordFlagOn = await configService.getFeatureFlag( + FeatureFlag.PM16117_ChangeExistingPasswordRefactor, + ); + + // User JIT provisioned into a master-password-encryption org + if ( + authStatus === AuthenticationStatus.Locked && + forceSetPasswordReason === ForceSetPasswordReason.SsoNewJitProvisionedUser && + !routerState.url.includes("set-initial-password") && + isSetInitialPasswordFlagOn + ) { + return router.createUrlTree(["/set-initial-password"]); + } + + if ( + authStatus === AuthenticationStatus.Locked && + forceSetPasswordReason !== ForceSetPasswordReason.SsoNewJitProvisionedUser + ) { if (routerState != null) { messagingService.send("lockedUrl", { url: routerState.url }); } @@ -55,18 +80,6 @@ export const authGuard: CanActivateFn = async ( return router.createUrlTree(["/remove-password"]); } - const userId = (await firstValueFrom(accountService.activeAccount$)).id; - const forceSetPasswordReason = await firstValueFrom( - masterPasswordService.forceSetPasswordReason$(userId), - ); - - const isSetInitialPasswordFlagOn = await configService.getFeatureFlag( - FeatureFlag.PM16117_SetInitialPasswordRefactor, - ); - const isChangePasswordFlagOn = await configService.getFeatureFlag( - FeatureFlag.PM16117_ChangeExistingPasswordRefactor, - ); - // TDE org user has "manage account recovery" permission if ( forceSetPasswordReason === diff --git a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts new file mode 100644 index 00000000000..dd81f560939 --- /dev/null +++ b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts @@ -0,0 +1,290 @@ +import { firstValueFrom } from "rxjs"; + +// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. +// eslint-disable-next-line no-restricted-imports +import { + OrganizationUserApiService, + OrganizationUserResetPasswordEnrollmentRequest, +} from "@bitwarden/admin-console/common"; +// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. +// eslint-disable-next-line no-restricted-imports +import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; +import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; +import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { UserId } from "@bitwarden/common/types/guid"; +import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { KdfConfigService, KeyService, KdfConfig } from "@bitwarden/key-management"; + +import { + SetInitialPasswordService, + SetInitialPasswordCredentials, + SetInitialPasswordUserType, + SetInitialPasswordTdeOffboardingCredentials, +} from "./set-initial-password.service.abstraction"; + +export class DefaultSetInitialPasswordService implements SetInitialPasswordService { + constructor( + protected apiService: ApiService, + protected encryptService: EncryptService, + protected i18nService: I18nService, + protected kdfConfigService: KdfConfigService, + protected keyService: KeyService, + protected masterPasswordApiService: MasterPasswordApiService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, + protected organizationApiService: OrganizationApiServiceAbstraction, + protected organizationUserApiService: OrganizationUserApiService, + protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, + ) {} + + async setInitialPassword( + credentials: SetInitialPasswordCredentials, + userType: SetInitialPasswordUserType, + userId: UserId, + ): Promise { + const { + newMasterKey, + newServerMasterKeyHash, + newLocalMasterKeyHash, + newPasswordHint, + kdfConfig, + orgSsoIdentifier, + orgId, + resetPasswordAutoEnroll, + } = credentials; + + for (const [key, value] of Object.entries(credentials)) { + if (value == null) { + throw new Error(`${key} not found. Could not set password.`); + } + } + if (userId == null) { + throw new Error("userId not found. Could not set password."); + } + if (userType == null) { + throw new Error("userType not found. Could not set password."); + } + + const masterKeyEncryptedUserKey = await this.makeMasterKeyEncryptedUserKey( + newMasterKey, + userId, + ); + if (masterKeyEncryptedUserKey == null || !masterKeyEncryptedUserKey[1].encryptedString) { + throw new Error("masterKeyEncryptedUserKey not found. Could not set password."); + } + + let keyPair: [string, EncString] | null = null; + let keysRequest: KeysRequest | null = null; + + if (userType === SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER) { + /** + * A user being JIT provisioned into a MP encryption org does not yet have a user + * asymmetric key pair, so we create it for them here. + * + * Sidenote: + * In the case of a TDE user whose permissions require that they have a MP - that user + * will already have a user asymmetric key pair by this point, so we skip this if-block + * so that we don't create a new key pair for them. + */ + + // Extra safety check (see description on https://github.com/bitwarden/clients/pull/10180): + // In case we have have a local private key and are not sure whether it has been posted to the server, + // we post the local private key instead of generating a new one + const existingUserPrivateKey = (await firstValueFrom( + this.keyService.userPrivateKey$(userId), + )) as Uint8Array; + + const existingUserPublicKey = await firstValueFrom(this.keyService.userPublicKey$(userId)); + + if (existingUserPrivateKey != null && existingUserPublicKey != null) { + const existingUserPublicKeyB64 = Utils.fromBufferToB64(existingUserPublicKey); + + // Existing key pair + keyPair = [ + existingUserPublicKeyB64, + await this.encryptService.wrapDecapsulationKey( + existingUserPrivateKey, + masterKeyEncryptedUserKey[0], + ), + ]; + } else { + // New key pair + keyPair = await this.keyService.makeKeyPair(masterKeyEncryptedUserKey[0]); + } + + if (keyPair == null) { + throw new Error("keyPair not found. Could not set password."); + } + if (!keyPair[1].encryptedString) { + throw new Error("encrypted private key not found. Could not set password."); + } + + keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString); + } + + const request = new SetPasswordRequest( + newServerMasterKeyHash, + masterKeyEncryptedUserKey[1].encryptedString, + newPasswordHint, + orgSsoIdentifier, + keysRequest, + kdfConfig.kdfType, + kdfConfig.iterations, + ); + + await this.masterPasswordApiService.setPassword(request); + + // Clear force set password reason to allow navigation back to vault. + await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId); + + // User now has a password so update account decryption options in state + await this.updateAccountDecryptionProperties( + newMasterKey, + kdfConfig, + masterKeyEncryptedUserKey, + userId, + ); + + /** + * Set the private key only for new JIT provisioned users in MP encryption orgs. + * (Existing TDE users will have their private key set on sync or on login.) + */ + if (keyPair != null && userType === SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER) { + if (!keyPair[1].encryptedString) { + throw new Error("encrypted private key not found. Could not set private key in state."); + } + await this.keyService.setPrivateKey(keyPair[1].encryptedString, userId); + } + + await this.masterPasswordService.setMasterKeyHash(newLocalMasterKeyHash, userId); + + if (resetPasswordAutoEnroll) { + await this.handleResetPasswordAutoEnroll(newServerMasterKeyHash, orgId, userId); + } + } + + private async makeMasterKeyEncryptedUserKey( + masterKey: MasterKey, + userId: UserId, + ): Promise<[UserKey, EncString]> { + let masterKeyEncryptedUserKey: [UserKey, EncString] | null = null; + + const userKey = await firstValueFrom(this.keyService.userKey$(userId)); + + if (userKey == null) { + masterKeyEncryptedUserKey = await this.keyService.makeUserKey(masterKey); + } else { + masterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(masterKey); + } + + return masterKeyEncryptedUserKey; + } + + private async updateAccountDecryptionProperties( + masterKey: MasterKey, + kdfConfig: KdfConfig, + masterKeyEncryptedUserKey: [UserKey, EncString], + userId: UserId, + ) { + const userDecryptionOpts = await firstValueFrom( + this.userDecryptionOptionsService.userDecryptionOptions$, + ); + userDecryptionOpts.hasMasterPassword = true; + await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts); + await this.kdfConfigService.setKdfConfig(userId, kdfConfig); + await this.masterPasswordService.setMasterKey(masterKey, userId); + await this.keyService.setUserKey(masterKeyEncryptedUserKey[0], userId); + } + + private async handleResetPasswordAutoEnroll( + masterKeyHash: string, + orgId: string, + userId: UserId, + ) { + const organizationKeys = await this.organizationApiService.getKeys(orgId); + + if (organizationKeys == null) { + throw new Error( + "Organization keys response is null. Could not handle reset password auto enroll.", + ); + } + + const orgPublicKey = Utils.fromB64ToArray(organizationKeys.publicKey); + const userKey = await firstValueFrom(this.keyService.userKey$(userId)); + + if (userKey == null) { + throw new Error("userKey not found. Could not handle reset password auto enroll."); + } + + // RSA encrypt user key with organization public key + const orgPublicKeyEncryptedUserKey = await this.encryptService.encapsulateKeyUnsigned( + userKey, + orgPublicKey, + ); + + if (orgPublicKeyEncryptedUserKey == null || !orgPublicKeyEncryptedUserKey.encryptedString) { + throw new Error( + "orgPublicKeyEncryptedUserKey not found. Could not handle reset password auto enroll.", + ); + } + + const enrollmentRequest = new OrganizationUserResetPasswordEnrollmentRequest(); + enrollmentRequest.masterPasswordHash = masterKeyHash; + enrollmentRequest.resetPasswordKey = orgPublicKeyEncryptedUserKey.encryptedString; + + await this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment( + orgId, + userId, + enrollmentRequest, + ); + } + + async setInitialPasswordTdeOffboarding( + credentials: SetInitialPasswordTdeOffboardingCredentials, + userId: UserId, + ) { + const { newMasterKey, newServerMasterKeyHash, newPasswordHint } = credentials; + for (const [key, value] of Object.entries(credentials)) { + if (value == null) { + throw new Error(`${key} not found. Could not set password.`); + } + } + + if (userId == null) { + throw new Error("userId not found. Could not set password."); + } + + const userKey = await firstValueFrom(this.keyService.userKey$(userId)); + if (userKey == null) { + throw new Error("userKey not found. Could not set password."); + } + + const newMasterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey( + newMasterKey, + userKey, + ); + + if (!newMasterKeyEncryptedUserKey[1].encryptedString) { + throw new Error("newMasterKeyEncryptedUserKey not found. Could not set password."); + } + + const request = new UpdateTdeOffboardingPasswordRequest(); + request.key = newMasterKeyEncryptedUserKey[1].encryptedString; + request.newMasterPasswordHash = newServerMasterKeyHash; + request.masterPasswordHint = newPasswordHint; + + await this.masterPasswordApiService.putUpdateTdeOffboardingPassword(request); + + // Clear force set password reason to allow navigation back to vault. + await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId); + } +} diff --git a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts new file mode 100644 index 00000000000..979dc5ee82f --- /dev/null +++ b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts @@ -0,0 +1,745 @@ +import { MockProxy, mock } from "jest-mock-extended"; +import { BehaviorSubject, of } from "rxjs"; + +// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. +// eslint-disable-next-line no-restricted-imports +import { + OrganizationUserApiService, + OrganizationUserResetPasswordEnrollmentRequest, +} from "@bitwarden/admin-console/common"; +// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. +// eslint-disable-next-line no-restricted-imports +import { + FakeUserDecryptionOptions as UserDecryptionOptions, + InternalUserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response"; +import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; +import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; +import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { UserId } from "@bitwarden/common/types/guid"; +import { MasterKey, UserKey, UserPrivateKey, UserPublicKey } from "@bitwarden/common/types/key"; +import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management"; + +import { DefaultSetInitialPasswordService } from "./default-set-initial-password.service.implementation"; +import { + SetInitialPasswordCredentials, + SetInitialPasswordService, + SetInitialPasswordTdeOffboardingCredentials, + SetInitialPasswordUserType, +} from "./set-initial-password.service.abstraction"; + +describe("DefaultSetInitialPasswordService", () => { + let sut: SetInitialPasswordService; + + let apiService: MockProxy; + let encryptService: MockProxy; + let i18nService: MockProxy; + let kdfConfigService: MockProxy; + let keyService: MockProxy; + let masterPasswordApiService: MockProxy; + let masterPasswordService: MockProxy; + let organizationApiService: MockProxy; + let organizationUserApiService: MockProxy; + let userDecryptionOptionsService: MockProxy; + + let userId: UserId; + let userKey: UserKey; + let userKeyEncString: EncString; + let masterKeyEncryptedUserKey: [UserKey, EncString]; + + beforeEach(() => { + apiService = mock(); + encryptService = mock(); + i18nService = mock(); + kdfConfigService = mock(); + keyService = mock(); + masterPasswordApiService = mock(); + masterPasswordService = mock(); + organizationApiService = mock(); + organizationUserApiService = mock(); + userDecryptionOptionsService = mock(); + + userId = "userId" as UserId; + userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; + userKeyEncString = new EncString("masterKeyEncryptedUserKey"); + masterKeyEncryptedUserKey = [userKey, userKeyEncString]; + + sut = new DefaultSetInitialPasswordService( + apiService, + encryptService, + i18nService, + kdfConfigService, + keyService, + masterPasswordApiService, + masterPasswordService, + organizationApiService, + organizationUserApiService, + userDecryptionOptionsService, + ); + }); + + it("should instantiate", () => { + expect(sut).not.toBeFalsy(); + }); + + describe("setInitialPassword(...)", () => { + // Mock function parameters + let credentials: SetInitialPasswordCredentials; + let userType: SetInitialPasswordUserType; + + // Mock other function data + let existingUserPublicKey: UserPublicKey; + let existingUserPrivateKey: UserPrivateKey; + let userKeyEncryptedPrivateKey: EncString; + + let keyPair: [string, EncString]; + let keysRequest: KeysRequest; + + let organizationKeys: OrganizationKeysResponse; + let orgPublicKeyEncryptedUserKey: EncString; + + let userDecryptionOptions: UserDecryptionOptions; + let userDecryptionOptionsSubject: BehaviorSubject; + let setPasswordRequest: SetPasswordRequest; + + let enrollmentRequest: OrganizationUserResetPasswordEnrollmentRequest; + + beforeEach(() => { + // Mock function parameters + credentials = { + newMasterKey: new SymmetricCryptoKey(new Uint8Array(32).buffer as CsprngArray) as MasterKey, + newServerMasterKeyHash: "newServerMasterKeyHash", + newLocalMasterKeyHash: "newLocalMasterKeyHash", + newPasswordHint: "newPasswordHint", + kdfConfig: DEFAULT_KDF_CONFIG, + orgSsoIdentifier: "orgSsoIdentifier", + orgId: "orgId", + resetPasswordAutoEnroll: false, + }; + userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER; + + // Mock other function data + existingUserPublicKey = Utils.fromB64ToArray("existingUserPublicKey") as UserPublicKey; + existingUserPrivateKey = Utils.fromB64ToArray("existingUserPrivateKey") as UserPrivateKey; + userKeyEncryptedPrivateKey = new EncString("userKeyEncryptedPrivateKey"); + + keyPair = ["publicKey", new EncString("privateKey")]; + keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString); + + organizationKeys = { + privateKey: "orgPrivateKey", + publicKey: "orgPublicKey", + } as OrganizationKeysResponse; + orgPublicKeyEncryptedUserKey = new EncString("orgPublicKeyEncryptedUserKey"); + + userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true }); + userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions); + userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject; + + setPasswordRequest = new SetPasswordRequest( + credentials.newServerMasterKeyHash, + masterKeyEncryptedUserKey[1].encryptedString, + credentials.newPasswordHint, + credentials.orgSsoIdentifier, + keysRequest, + credentials.kdfConfig.kdfType, + credentials.kdfConfig.iterations, + ); + + enrollmentRequest = new OrganizationUserResetPasswordEnrollmentRequest(); + enrollmentRequest.masterPasswordHash = credentials.newServerMasterKeyHash; + enrollmentRequest.resetPasswordKey = orgPublicKeyEncryptedUserKey.encryptedString; + }); + + interface MockConfig { + userType: SetInitialPasswordUserType; + userHasUserKey: boolean; + userHasLocalKeyPair: boolean; + resetPasswordAutoEnroll: boolean; + } + + const defaultMockConfig: MockConfig = { + userType: SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER, + userHasUserKey: true, + userHasLocalKeyPair: false, + resetPasswordAutoEnroll: false, + }; + + function setupMocks(config: MockConfig = defaultMockConfig) { + // Mock makeMasterKeyEncryptedUserKey() values + if (config.userHasUserKey) { + keyService.userKey$.mockReturnValue(of(userKey)); + keyService.encryptUserKeyWithMasterKey.mockResolvedValue(masterKeyEncryptedUserKey); + } else { + keyService.userKey$.mockReturnValue(of(null)); + keyService.makeUserKey.mockResolvedValue(masterKeyEncryptedUserKey); + } + + // Mock keyPair values + if (config.userType === SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER) { + if (config.userHasLocalKeyPair) { + keyService.userPrivateKey$.mockReturnValue(of(existingUserPrivateKey)); + keyService.userPublicKey$.mockReturnValue(of(existingUserPublicKey)); + encryptService.wrapDecapsulationKey.mockResolvedValue(userKeyEncryptedPrivateKey); + } else { + keyService.userPrivateKey$.mockReturnValue(of(null)); + keyService.userPublicKey$.mockReturnValue(of(null)); + keyService.makeKeyPair.mockResolvedValue(keyPair); + } + } + + // Mock handleResetPasswordAutoEnroll() values + if (config.resetPasswordAutoEnroll) { + organizationApiService.getKeys.mockResolvedValue(organizationKeys); + encryptService.encapsulateKeyUnsigned.mockResolvedValue(orgPublicKeyEncryptedUserKey); + keyService.userKey$.mockReturnValue(of(userKey)); + } + } + + describe("general error handling", () => { + [ + "newMasterKey", + "newServerMasterKeyHash", + "newLocalMasterKeyHash", + "newPasswordHint", + "kdfConfig", + "orgSsoIdentifier", + "orgId", + "resetPasswordAutoEnroll", + ].forEach((key) => { + it(`should throw if ${key} is not provided on the SetInitialPasswordCredentials object`, async () => { + // Arrange + const invalidCredentials: SetInitialPasswordCredentials = { + ...credentials, + [key]: null, + }; + + // Act + const promise = sut.setInitialPassword(invalidCredentials, userType, userId); + + // Assert + await expect(promise).rejects.toThrow(`${key} not found. Could not set password.`); + }); + }); + + ["userId", "userType"].forEach((param) => { + it(`should throw if ${param} was not passed in`, async () => { + // Arrange & Act + const promise = sut.setInitialPassword( + credentials, + param === "userType" ? null : userType, + param === "userId" ? null : userId, + ); + + // Assert + await expect(promise).rejects.toThrow(`${param} not found. Could not set password.`); + }); + }); + }); + + describe("given SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER", () => { + beforeEach(() => { + userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER; + }); + + describe("given the user has an existing local key pair", () => { + it("should NOT create a brand new key pair for the user", async () => { + // Arrange + setPasswordRequest.keys = { + encryptedPrivateKey: userKeyEncryptedPrivateKey.encryptedString, + publicKey: Utils.fromBufferToB64(existingUserPublicKey), + }; + + setupMocks({ ...defaultMockConfig, userHasLocalKeyPair: true }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect(keyService.userPrivateKey$).toHaveBeenCalledWith(userId); + expect(keyService.userPublicKey$).toHaveBeenCalledWith(userId); + expect(encryptService.wrapDecapsulationKey).toHaveBeenCalledWith( + existingUserPrivateKey, + masterKeyEncryptedUserKey[0], + ); + expect(keyService.makeKeyPair).not.toHaveBeenCalled(); + }); + }); + + describe("given the user has a userKey", () => { + it("should successfully set an initial password", async () => { + // Arrange + setupMocks(); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + }); + }); + + describe("given the user does NOT have a userKey", () => { + it("should successfully set an initial password", async () => { + // Arrange + setupMocks({ ...defaultMockConfig, userHasUserKey: false }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + }); + }); + + it("should throw if a key pair is not found", async () => { + // Arrange + keyPair = null; + + setupMocks(); + + // Act + const promise = sut.setInitialPassword(credentials, userType, userId); + + // Assert + await expect(promise).rejects.toThrow("keyPair not found. Could not set password."); + expect(masterPasswordApiService.setPassword).not.toHaveBeenCalled(); + }); + + it("should throw if an encrypted private key is not found", async () => { + // Arrange + keyPair[1].encryptedString = "" as EncryptedString; + + setupMocks(); + + // Act + const promise = sut.setInitialPassword(credentials, userType, userId); + + // Assert + await expect(promise).rejects.toThrow( + "encrypted private key not found. Could not set password.", + ); + expect(masterPasswordApiService.setPassword).not.toHaveBeenCalled(); + }); + + describe("given the initial password has been successfully set", () => { + it("should clear the ForceSetPasswordReason by setting it to None", async () => { + // Arrange + setupMocks(); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect(masterPasswordService.setForceSetPasswordReason).toHaveBeenCalledWith( + ForceSetPasswordReason.None, + userId, + ); + }); + + it("should update account decryption properties", async () => { + // Arrange + setupMocks(); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith( + userDecryptionOptions, + ); + expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig); + expect(masterPasswordService.setMasterKey).toHaveBeenCalledWith( + credentials.newMasterKey, + userId, + ); + expect(keyService.setUserKey).toHaveBeenCalledWith(masterKeyEncryptedUserKey[0], userId); + }); + + it("should set the private key to state", async () => { + // Arrange + setupMocks(); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect(keyService.setPrivateKey).toHaveBeenCalledWith(keyPair[1].encryptedString, userId); + }); + + it("should set the local master key hash to state", async () => { + // Arrange + setupMocks(); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect(masterPasswordService.setMasterKeyHash).toHaveBeenCalledWith( + credentials.newLocalMasterKeyHash, + userId, + ); + }); + + describe("given resetPasswordAutoEnroll is true", () => { + it(`should handle reset password (account recovery) auto enroll`, async () => { + // Arrange + credentials.resetPasswordAutoEnroll = true; + + setupMocks({ ...defaultMockConfig, resetPasswordAutoEnroll: true }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect( + organizationUserApiService.putOrganizationUserResetPasswordEnrollment, + ).toHaveBeenCalledWith(credentials.orgId, userId, enrollmentRequest); + }); + + it("should throw if organization keys are not found", async () => { + // Arrange + credentials.resetPasswordAutoEnroll = true; + organizationKeys = null; + + setupMocks({ ...defaultMockConfig, resetPasswordAutoEnroll: true }); + + // Act + const promise = sut.setInitialPassword(credentials, userType, userId); + + // Assert + await expect(promise).rejects.toThrow( + "Organization keys response is null. Could not handle reset password auto enroll.", + ); + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect( + organizationUserApiService.putOrganizationUserResetPasswordEnrollment, + ).not.toHaveBeenCalled(); + }); + + ["orgPublicKeyEncryptedUserKey", "orgPublicKeyEncryptedUserKey.encryptedString"].forEach( + (property) => { + it("should throw if orgPublicKeyEncryptedUserKey is not found", async () => { + // Arrange + credentials.resetPasswordAutoEnroll = true; + + if (property === "orgPublicKeyEncryptedUserKey") { + orgPublicKeyEncryptedUserKey = null; + } else { + orgPublicKeyEncryptedUserKey.encryptedString = "" as EncryptedString; + } + + setupMocks({ ...defaultMockConfig, resetPasswordAutoEnroll: true }); + + // Act + const promise = sut.setInitialPassword(credentials, userType, userId); + + // Assert + await expect(promise).rejects.toThrow( + "orgPublicKeyEncryptedUserKey not found. Could not handle reset password auto enroll.", + ); + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith( + setPasswordRequest, + ); + expect( + organizationUserApiService.putOrganizationUserResetPasswordEnrollment, + ).not.toHaveBeenCalled(); + }); + }, + ); + }); + + describe("given resetPasswordAutoEnroll is false", () => { + it(`should NOT handle reset password (account recovery) auto enroll`, async () => { + // Arrange + credentials.resetPasswordAutoEnroll = false; + + setupMocks(); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect( + organizationUserApiService.putOrganizationUserResetPasswordEnrollment, + ).not.toHaveBeenCalled(); + }); + }); + }); + }); + + describe("given SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP", () => { + beforeEach(() => { + userType = SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP; + setPasswordRequest.keys = null; + }); + + it("should NOT generate a keyPair", async () => { + // Arrange + setupMocks({ ...defaultMockConfig, userType }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(keyService.userPrivateKey$).not.toHaveBeenCalled(); + expect(keyService.userPublicKey$).not.toHaveBeenCalled(); + expect(encryptService.wrapDecapsulationKey).not.toHaveBeenCalled(); + expect(keyService.makeKeyPair).not.toHaveBeenCalled(); + }); + + describe("given the user has a userKey", () => { + it("should successfully set an initial password", async () => { + // Arrange + setupMocks({ ...defaultMockConfig, userType }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + }); + }); + + describe("given the user does NOT have a userKey", () => { + it("should successfully set an initial password", async () => { + // Arrange + setupMocks({ ...defaultMockConfig, userType }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + }); + }); + + describe("given the initial password has been successfully set", () => { + it("should clear the ForceSetPasswordReason by setting it to None", async () => { + // Arrange + setupMocks({ ...defaultMockConfig, userType }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect(masterPasswordService.setForceSetPasswordReason).toHaveBeenCalledWith( + ForceSetPasswordReason.None, + userId, + ); + }); + + it("should update account decryption properties", async () => { + // Arrange + setupMocks({ ...defaultMockConfig, userType }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith( + userDecryptionOptions, + ); + expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig); + expect(masterPasswordService.setMasterKey).toHaveBeenCalledWith( + credentials.newMasterKey, + userId, + ); + expect(keyService.setUserKey).toHaveBeenCalledWith(masterKeyEncryptedUserKey[0], userId); + }); + + it("should NOT set the private key to state", async () => { + // Arrange + setupMocks({ ...defaultMockConfig, userType }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect(keyService.setPrivateKey).not.toHaveBeenCalled(); + }); + + it("should set the local master key hash to state", async () => { + // Arrange + setupMocks({ ...defaultMockConfig, userType }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect(masterPasswordService.setMasterKeyHash).toHaveBeenCalledWith( + credentials.newLocalMasterKeyHash, + userId, + ); + }); + + describe("given resetPasswordAutoEnroll is true", () => { + it(`should handle reset password (account recovery) auto enroll`, async () => { + // Arrange + credentials.resetPasswordAutoEnroll = true; + + setupMocks({ ...defaultMockConfig, userType, resetPasswordAutoEnroll: true }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect( + organizationUserApiService.putOrganizationUserResetPasswordEnrollment, + ).toHaveBeenCalledWith(credentials.orgId, userId, enrollmentRequest); + }); + }); + + describe("given resetPasswordAutoEnroll is false", () => { + it(`should NOT handle reset password (account recovery) auto enroll`, async () => { + // Arrange + setupMocks({ ...defaultMockConfig, userType }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect( + organizationUserApiService.putOrganizationUserResetPasswordEnrollment, + ).not.toHaveBeenCalled(); + }); + }); + }); + }); + }); + + describe("setInitialPasswordTdeOffboarding(...)", () => { + // Mock function parameters + let credentials: SetInitialPasswordTdeOffboardingCredentials; + + beforeEach(() => { + // Mock function parameters + credentials = { + newMasterKey: new SymmetricCryptoKey(new Uint8Array(32).buffer as CsprngArray) as MasterKey, + newServerMasterKeyHash: "newServerMasterKeyHash", + newPasswordHint: "newPasswordHint", + }; + }); + + function setupTdeOffboardingMocks() { + keyService.userKey$.mockReturnValue(of(userKey)); + keyService.encryptUserKeyWithMasterKey.mockResolvedValue(masterKeyEncryptedUserKey); + } + + it("should successfully set an initial password for the TDE offboarding user", async () => { + // Arrange + setupTdeOffboardingMocks(); + + const request = new UpdateTdeOffboardingPasswordRequest(); + request.key = masterKeyEncryptedUserKey[1].encryptedString; + request.newMasterPasswordHash = credentials.newServerMasterKeyHash; + request.masterPasswordHint = credentials.newPasswordHint; + + // Act + await sut.setInitialPasswordTdeOffboarding(credentials, userId); + + // Assert + expect(masterPasswordApiService.putUpdateTdeOffboardingPassword).toHaveBeenCalledTimes(1); + expect(masterPasswordApiService.putUpdateTdeOffboardingPassword).toHaveBeenCalledWith( + request, + ); + }); + + describe("given the initial password has been successfully set", () => { + it("should clear the ForceSetPasswordReason by setting it to None", async () => { + // Arrange + setupTdeOffboardingMocks(); + + // Act + await sut.setInitialPasswordTdeOffboarding(credentials, userId); + + // Assert + expect(masterPasswordApiService.putUpdateTdeOffboardingPassword).toHaveBeenCalledTimes(1); + expect(masterPasswordService.setForceSetPasswordReason).toHaveBeenCalledWith( + ForceSetPasswordReason.None, + userId, + ); + }); + }); + + describe("general error handling", () => { + ["newMasterKey", "newServerMasterKeyHash", "newPasswordHint"].forEach((key) => { + it(`should throw if ${key} is not provided on the SetInitialPasswordTdeOffboardingCredentials object`, async () => { + // Arrange + const invalidCredentials: SetInitialPasswordTdeOffboardingCredentials = { + ...credentials, + [key]: null, + }; + + // Act + const promise = sut.setInitialPasswordTdeOffboarding(invalidCredentials, userId); + + // Assert + await expect(promise).rejects.toThrow(`${key} not found. Could not set password.`); + }); + }); + + it(`should throw if the userId was not passed in`, async () => { + // Arrange + userId = null; + + // Act + const promise = sut.setInitialPasswordTdeOffboarding(credentials, userId); + + // Assert + await expect(promise).rejects.toThrow("userId not found. Could not set password."); + }); + + it(`should throw if the userKey was not found`, async () => { + // Arrange + keyService.userKey$.mockReturnValue(of(null)); + + // Act + const promise = sut.setInitialPasswordTdeOffboarding(credentials, userId); + + // Assert + await expect(promise).rejects.toThrow("userKey not found. Could not set password."); + }); + + it(`should throw if a newMasterKeyEncryptedUserKey was not returned`, async () => { + // Arrange + masterKeyEncryptedUserKey[1].encryptedString = "" as EncryptedString; + + setupTdeOffboardingMocks(); + + // Act + const promise = sut.setInitialPasswordTdeOffboarding(credentials, userId); + + // Assert + await expect(promise).rejects.toThrow( + "newMasterKeyEncryptedUserKey not found. Could not set password.", + ); + }); + }); + }); +}); diff --git a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.html b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.html new file mode 100644 index 00000000000..4956f293d1e --- /dev/null +++ b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.html @@ -0,0 +1,34 @@ +@if (initializing) { +
    + +
    +} @else { + + {{ "resetPasswordAutoEnrollInviteWarning" | i18n }} + + + +} diff --git a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts new file mode 100644 index 00000000000..2de9aaf7b75 --- /dev/null +++ b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts @@ -0,0 +1,307 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnInit } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; + +// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. +// eslint-disable-next-line no-restricted-imports +import { + InputPasswordComponent, + InputPasswordFlow, + PasswordInputResult, +} from "@bitwarden/auth/angular"; +// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. +// eslint-disable-next-line no-restricted-imports +import { LogoutService } from "@bitwarden/auth/common"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; +import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { assertTruthy, assertNonNullish } from "@bitwarden/common/auth/utils"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; +import { UserId } from "@bitwarden/common/types/guid"; +import { + AnonLayoutWrapperDataService, + CalloutComponent, + DialogService, + ToastService, +} from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; + +import { + SetInitialPasswordCredentials, + SetInitialPasswordService, + SetInitialPasswordTdeOffboardingCredentials, + SetInitialPasswordUserType, +} from "./set-initial-password.service.abstraction"; + +@Component({ + standalone: true, + templateUrl: "set-initial-password.component.html", + imports: [CalloutComponent, CommonModule, InputPasswordComponent, I18nPipe], +}) +export class SetInitialPasswordComponent implements OnInit { + protected inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAuthedUser; + + protected email?: string; + protected forceSetPasswordReason?: ForceSetPasswordReason; + protected initializing = true; + protected masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null; + protected orgId?: string; + protected orgSsoIdentifier?: string; + protected resetPasswordAutoEnroll?: boolean; + protected submitting = false; + protected userId?: UserId; + protected userType?: SetInitialPasswordUserType; + protected SetInitialPasswordUserType = SetInitialPasswordUserType; + + constructor( + private accountService: AccountService, + private activatedRoute: ActivatedRoute, + private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, + private dialogService: DialogService, + private i18nService: I18nService, + private logoutService: LogoutService, + private logService: LogService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, + private messagingService: MessagingService, + private organizationApiService: OrganizationApiServiceAbstraction, + private policyApiService: PolicyApiServiceAbstraction, + private policyService: PolicyService, + private router: Router, + private setInitialPasswordService: SetInitialPasswordService, + private ssoLoginService: SsoLoginServiceAbstraction, + private syncService: SyncService, + private toastService: ToastService, + private validationService: ValidationService, + ) {} + + async ngOnInit() { + await this.syncService.fullSync(true); + + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + this.userId = activeAccount?.id; + this.email = activeAccount?.email; + + await this.establishUserType(); + await this.getOrgInfo(); + + this.initializing = false; + } + + private async establishUserType() { + if (!this.userId) { + throw new Error("userId not found. Could not determine user type."); + } + + this.forceSetPasswordReason = await firstValueFrom( + this.masterPasswordService.forceSetPasswordReason$(this.userId), + ); + + if (this.forceSetPasswordReason === ForceSetPasswordReason.SsoNewJitProvisionedUser) { + this.userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER; + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { key: "joinOrganization" }, + pageSubtitle: { key: "finishJoiningThisOrganizationBySettingAMasterPassword" }, + }); + } + + if ( + this.forceSetPasswordReason === + ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission + ) { + this.userType = SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP; + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { key: "setMasterPassword" }, + pageSubtitle: { key: "orgPermissionsUpdatedMustSetPassword" }, + }); + } + + if (this.forceSetPasswordReason === ForceSetPasswordReason.TdeOffboarding) { + this.userType = SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER; + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { key: "setMasterPassword" }, + pageSubtitle: { key: "tdeDisabledMasterPasswordRequired" }, + }); + } + + // If we somehow end up here without a reason, navigate to root + if (this.forceSetPasswordReason === ForceSetPasswordReason.None) { + await this.router.navigate(["/"]); + } + } + + private async getOrgInfo() { + if (!this.userId) { + throw new Error("userId not found. Could not handle query params."); + } + + if (this.userType === SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER) { + this.masterPasswordPolicyOptions = + (await firstValueFrom(this.policyService.masterPasswordPolicyOptions$(this.userId))) ?? + null; + + return; + } + + const qParams = await firstValueFrom(this.activatedRoute.queryParams); + + this.orgSsoIdentifier = + qParams.identifier ?? + (await this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.userId)); + + if (this.orgSsoIdentifier != null) { + try { + const autoEnrollStatus = await this.organizationApiService.getAutoEnrollStatus( + this.orgSsoIdentifier, + ); + this.orgId = autoEnrollStatus.id; + this.resetPasswordAutoEnroll = autoEnrollStatus.resetPasswordEnabled; + this.masterPasswordPolicyOptions = + await this.policyApiService.getMasterPasswordPolicyOptsForOrgUser(this.orgId); + } catch { + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t("errorOccurred"), + }); + } + } + } + + protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) { + this.submitting = true; + + switch (this.userType) { + case SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER: + case SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP: + await this.setInitialPassword(passwordInputResult); + break; + case SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER: + await this.setInitialPasswordTdeOffboarding(passwordInputResult); + break; + default: + this.logService.error( + `Unexpected user type: ${this.userType}. Could not set initial password.`, + ); + this.validationService.showError("Unexpected user type. Could not set initial password."); + } + } + + private async setInitialPassword(passwordInputResult: PasswordInputResult) { + const ctx = "Could not set initial password."; + assertTruthy(passwordInputResult.newMasterKey, "newMasterKey", ctx); + assertTruthy(passwordInputResult.newServerMasterKeyHash, "newServerMasterKeyHash", ctx); + assertTruthy(passwordInputResult.newLocalMasterKeyHash, "newLocalMasterKeyHash", ctx); + assertTruthy(passwordInputResult.kdfConfig, "kdfConfig", ctx); + assertTruthy(this.orgSsoIdentifier, "orgSsoIdentifier", ctx); + assertTruthy(this.orgId, "orgId", ctx); + assertTruthy(this.userType, "userType", ctx); + assertTruthy(this.userId, "userId", ctx); + assertNonNullish(passwordInputResult.newPasswordHint, "newPasswordHint", ctx); // can have an empty string as a valid value, so check non-nullish + assertNonNullish(this.resetPasswordAutoEnroll, "resetPasswordAutoEnroll", ctx); // can have `false` as a valid value, so check non-nullish + + try { + const credentials: SetInitialPasswordCredentials = { + newMasterKey: passwordInputResult.newMasterKey, + newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash, + newLocalMasterKeyHash: passwordInputResult.newLocalMasterKeyHash, + newPasswordHint: passwordInputResult.newPasswordHint, + kdfConfig: passwordInputResult.kdfConfig, + orgSsoIdentifier: this.orgSsoIdentifier, + orgId: this.orgId, + resetPasswordAutoEnroll: this.resetPasswordAutoEnroll, + }; + + await this.setInitialPasswordService.setInitialPassword( + credentials, + this.userType, + this.userId, + ); + + this.showSuccessToastByUserType(); + + this.submitting = false; + await this.router.navigate(["vault"]); + } catch (e) { + this.logService.error("Error setting initial password", e); + this.validationService.showError(e); + this.submitting = false; + } + } + + private async setInitialPasswordTdeOffboarding(passwordInputResult: PasswordInputResult) { + const ctx = "Could not set initial password."; + assertTruthy(passwordInputResult.newMasterKey, "newMasterKey", ctx); + assertTruthy(passwordInputResult.newServerMasterKeyHash, "newServerMasterKeyHash", ctx); + assertTruthy(this.userId, "userId", ctx); + assertNonNullish(passwordInputResult.newPasswordHint, "newPasswordHint", ctx); // can have an empty string as a valid value, so check non-nullish + + try { + const credentials: SetInitialPasswordTdeOffboardingCredentials = { + newMasterKey: passwordInputResult.newMasterKey, + newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash, + newPasswordHint: passwordInputResult.newPasswordHint, + }; + + await this.setInitialPasswordService.setInitialPasswordTdeOffboarding( + credentials, + this.userId, + ); + + this.showSuccessToastByUserType(); + + await this.logoutService.logout(this.userId); + // navigate to root so redirect guard can properly route next active user or null user to correct page + await this.router.navigate(["/"]); + } catch (e) { + this.logService.error("Error setting initial password during TDE offboarding", e); + this.validationService.showError(e); + } finally { + this.submitting = false; + } + } + + private showSuccessToastByUserType() { + if (this.userType === SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER) { + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("accountSuccessfullyCreated"), + }); + + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("inviteAccepted"), + }); + } else { + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("masterPasswordSuccessfullySet"), + }); + } + } + + protected async logout() { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "logOut" }, + content: { key: "logOutConfirmation" }, + acceptButtonText: { key: "logOut" }, + type: "warning", + }); + + if (confirmed) { + this.messagingService.send("logout"); + } + } +} diff --git a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts new file mode 100644 index 00000000000..c167c1675c1 --- /dev/null +++ b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts @@ -0,0 +1,89 @@ +import { UserId } from "@bitwarden/common/types/guid"; +import { MasterKey } from "@bitwarden/common/types/key"; +import { KdfConfig } from "@bitwarden/key-management"; + +export const _SetInitialPasswordUserType = { + /** + * A user being "just-in-time" (JIT) provisioned into a master-password-encryption org + */ + JIT_PROVISIONED_MP_ORG_USER: "jit_provisioned_mp_org_user", + + /** + * Could be one of two scenarios: + * 1. A user being "just-in-time" (JIT) provisioned into a trusted-device-encryption org + * with the reset password permission granted ("manage account recovery"), which requires + * that the user sets a master password + * 2. An user in a trusted-device-encryption org whose permissions were upgraded to include + * the reset password permission ("manage account recovery"), which requires that the user + * sets a master password + */ + TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP: + "tde_org_user_reset_password_permission_requires_mp", + + /** + * A user in an org that offboarded from trusted device encryption and is now a + * master-password-encryption org + */ + OFFBOARDED_TDE_ORG_USER: "offboarded_tde_org_user", +} as const; + +type _SetInitialPasswordUserType = typeof _SetInitialPasswordUserType; + +export type SetInitialPasswordUserType = + _SetInitialPasswordUserType[keyof _SetInitialPasswordUserType]; +export const SetInitialPasswordUserType: Readonly<{ + [K in keyof typeof _SetInitialPasswordUserType]: SetInitialPasswordUserType; +}> = Object.freeze(_SetInitialPasswordUserType); + +export interface SetInitialPasswordCredentials { + newMasterKey: MasterKey; + newServerMasterKeyHash: string; + newLocalMasterKeyHash: string; + newPasswordHint: string; + kdfConfig: KdfConfig; + orgSsoIdentifier: string; + orgId: string; + resetPasswordAutoEnroll: boolean; +} + +export interface SetInitialPasswordTdeOffboardingCredentials { + newMasterKey: MasterKey; + newServerMasterKeyHash: string; + newPasswordHint: string; +} + +/** + * Handles setting an initial password for an existing authed user. + * + * To see the different scenarios where an existing authed user needs to set an + * initial password, see {@link SetInitialPasswordUserType} + */ +export abstract class SetInitialPasswordService { + /** + * Sets an initial password for an existing authed user who is either: + * - {@link SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER} + * - {@link SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP} + * + * @param credentials An object of the credentials needed to set the initial password + * @throws If any property on the `credentials` object is null or undefined, or if a + * masterKeyEncryptedUserKey or newKeyPair could not be created. + */ + abstract setInitialPassword: ( + credentials: SetInitialPasswordCredentials, + userType: SetInitialPasswordUserType, + userId: UserId, + ) => Promise; + + /** + * Sets an initial password for a user who logs in after their org offboarded from + * trusted device encryption and is now a master-password-encryption org: + * - {@link SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER} + * + * @param passwordInputResult credentials object received from the `InputPasswordComponent` + * @param userId the account `userId` + */ + abstract setInitialPasswordTdeOffboarding: ( + credentials: SetInitialPasswordTdeOffboardingCredentials, + userId: UserId, + ) => Promise; +} diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 780604f048d..96a95de501e 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -339,6 +339,8 @@ import { VaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; +import { DefaultSetInitialPasswordService } from "../auth/password-management/set-initial-password/default-set-initial-password.service.implementation"; +import { SetInitialPasswordService } from "../auth/password-management/set-initial-password/set-initial-password.service.abstraction"; import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction"; import { DeviceTrustToastService } from "../auth/services/device-trust-toast.service.implementation"; import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service"; @@ -1419,6 +1421,22 @@ const safeProviders: SafeProvider[] = [ InternalUserDecryptionOptionsServiceAbstraction, ], }), + safeProvider({ + provide: SetInitialPasswordService, + useClass: DefaultSetInitialPasswordService, + deps: [ + ApiServiceAbstraction, + EncryptService, + I18nServiceAbstraction, + KdfConfigService, + KeyService, + MasterPasswordApiServiceAbstraction, + InternalMasterPasswordServiceAbstraction, + OrganizationApiServiceAbstraction, + OrganizationUserApiService, + InternalUserDecryptionOptionsServiceAbstraction, + ], + }), safeProvider({ provide: DefaultServerSettingsService, useClass: DefaultServerSettingsService, diff --git a/libs/angular/src/tools/send/add-edit.component.ts b/libs/angular/src/tools/send/add-edit.component.ts index 0289664c365..221b751528a 100644 --- a/libs/angular/src/tools/send/add-edit.component.ts +++ b/libs/angular/src/tools/send/add-edit.component.ts @@ -148,14 +148,6 @@ export class AddEditComponent implements OnInit, OnDestroy { return null; } - get isSafari() { - return this.platformUtilsService.isSafari(); - } - - get isDateTimeLocalSupported(): boolean { - return !(this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari()); - } - async ngOnInit() { this.accountService.activeAccount$ .pipe( diff --git a/libs/angular/src/vault/abstractions/deprecated-vault-filter.service.ts b/libs/angular/src/vault/abstractions/deprecated-vault-filter.service.ts index 21528b1ddd5..9a1a31b6068 100644 --- a/libs/angular/src/vault/abstractions/deprecated-vault-filter.service.ts +++ b/libs/angular/src/vault/abstractions/deprecated-vault-filter.service.ts @@ -6,6 +6,7 @@ import { Observable } from "rxjs"; // eslint-disable-next-line no-restricted-imports import { CollectionView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { UserId } from "@bitwarden/common/types/guid"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { DynamicTreeNode } from "../vault-filter/models/dynamic-tree-node.model"; @@ -14,11 +15,14 @@ import { DynamicTreeNode } from "../vault-filter/models/dynamic-tree-node.model" * @deprecated August 30 2022: Use new VaultFilterService with observables */ export abstract class DeprecatedVaultFilterService { - buildOrganizations: () => Promise; - buildNestedFolders: (organizationId?: string) => Observable>; - buildCollections: (organizationId?: string) => Promise>; - buildCollapsedFilterNodes: () => Promise>; - storeCollapsedFilterNodes: (collapsedFilterNodes: Set) => Promise; - checkForSingleOrganizationPolicy: () => Promise; - checkForOrganizationDataOwnershipPolicy: () => Promise; + abstract buildOrganizations(): Promise; + abstract buildNestedFolders(organizationId?: string): Observable>; + abstract buildCollections(organizationId?: string): Promise>; + abstract buildCollapsedFilterNodes(userId: UserId): Promise>; + abstract storeCollapsedFilterNodes( + collapsedFilterNodes: Set, + userId: UserId, + ): Promise; + abstract checkForSingleOrganizationPolicy(): Promise; + abstract checkForOrganizationDataOwnershipPolicy(): Promise; } diff --git a/libs/angular/src/vault/vault-filter/components/vault-filter.component.ts b/libs/angular/src/vault/vault-filter/components/vault-filter.component.ts index 936d606b936..0b0cb14bbb8 100644 --- a/libs/angular/src/vault/vault-filter/components/vault-filter.component.ts +++ b/libs/angular/src/vault/vault-filter/components/vault-filter.component.ts @@ -7,6 +7,9 @@ import { firstValueFrom, Observable } from "rxjs"; // eslint-disable-next-line no-restricted-imports import { CollectionView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; @@ -29,6 +32,8 @@ export class VaultFilterComponent implements OnInit { @Output() onAddFolder = new EventEmitter(); @Output() onEditFolder = new EventEmitter(); + private activeUserId: UserId; + isLoaded = false; collapsedFilterNodes: Set; organizations: Organization[]; @@ -37,14 +42,20 @@ export class VaultFilterComponent implements OnInit { collections: DynamicTreeNode; folders$: Observable>; - constructor(protected vaultFilterService: DeprecatedVaultFilterService) {} + constructor( + protected vaultFilterService: DeprecatedVaultFilterService, + protected accountService: AccountService, + ) {} get displayCollections() { return this.collections?.fullList != null && this.collections.fullList.length > 0; } async ngOnInit(): Promise { - this.collapsedFilterNodes = await this.vaultFilterService.buildCollapsedFilterNodes(); + this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + this.collapsedFilterNodes = await this.vaultFilterService.buildCollapsedFilterNodes( + this.activeUserId, + ); this.organizations = await this.vaultFilterService.buildOrganizations(); if (this.organizations != null && this.organizations.length > 0) { this.activeOrganizationDataOwnershipPolicy = @@ -68,7 +79,10 @@ export class VaultFilterComponent implements OnInit { } else { this.collapsedFilterNodes.add(node.id); } - await this.vaultFilterService.storeCollapsedFilterNodes(this.collapsedFilterNodes); + await this.vaultFilterService.storeCollapsedFilterNodes( + this.collapsedFilterNodes, + this.activeUserId, + ); } async applyFilter(filter: VaultFilter) { diff --git a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts index a4114e63285..3317f0c9002 100644 --- a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts +++ b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts @@ -12,7 +12,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { ActiveUserState, StateProvider } from "@bitwarden/common/platform/state"; +import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -28,10 +28,9 @@ const NestingDelimiter = "/"; @Injectable() export class VaultFilterService implements DeprecatedVaultFilterServiceAbstraction { - private collapsedGroupingsState: ActiveUserState = - this.stateProvider.getActive(COLLAPSED_GROUPINGS); - private readonly collapsedGroupings$: Observable> = - this.collapsedGroupingsState.state$.pipe(map((c) => new Set(c))); + private collapsedGroupingsState(userId: UserId): SingleUserState { + return this.stateProvider.getUser(userId, COLLAPSED_GROUPINGS); + } constructor( protected organizationService: OrganizationService, @@ -43,12 +42,17 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti protected accountService: AccountService, ) {} - async storeCollapsedFilterNodes(collapsedFilterNodes: Set): Promise { - await this.collapsedGroupingsState.update(() => Array.from(collapsedFilterNodes)); + async storeCollapsedFilterNodes( + collapsedFilterNodes: Set, + userId: UserId, + ): Promise { + await this.collapsedGroupingsState(userId).update(() => Array.from(collapsedFilterNodes)); } - async buildCollapsedFilterNodes(): Promise> { - return await firstValueFrom(this.collapsedGroupings$); + async buildCollapsedFilterNodes(userId: UserId): Promise> { + return await firstValueFrom( + this.collapsedGroupingsState(userId).state$.pipe(map((c) => new Set(c))), + ); } async buildOrganizations(): Promise { diff --git a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts index 5510cbe9f30..b116a62dd4d 100644 --- a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts +++ b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts @@ -25,6 +25,10 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi return null; } + determineLoginSuccessRoute(): Promise { + return Promise.resolve("/vault"); + } + async finishRegistration( email: string, passwordInputResult: PasswordInputResult, diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts index 7ef4d9690a7..1d1a2d8f892 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts @@ -10,7 +10,9 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; import { RegisterVerificationEmailClickedRequest } from "@bitwarden/common/auth/models/request/registration/register-verification-email-clicked.request"; import { HttpStatusCode } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; @@ -77,6 +79,7 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { private logService: LogService, private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, private loginSuccessHandlerService: LoginSuccessHandlerService, + private configService: ConfigService, ) {} async ngOnInit() { @@ -186,15 +189,23 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { return; } - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("youHaveBeenLoggedIn"), - }); + const endUserActivationFlagEnabled = await this.configService.getFeatureFlag( + FeatureFlag.PM19315EndUserActivationMvp, + ); + + if (!endUserActivationFlagEnabled) { + // Only show the toast when the end user activation feature flag is _not_ enabled + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("youHaveBeenLoggedIn"), + }); + } await this.loginSuccessHandlerService.run(authenticationResult.userId); - await this.router.navigate(["/vault"]); + const successRoute = await this.registrationFinishService.determineLoginSuccessRoute(); + await this.router.navigate([successRoute]); } catch (e) { // If login errors, redirect to login page per product. Don't show error this.logService.error("Error logging in after registration: ", e.message); diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts index 5f3c04e5155..523a4c79c54 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts @@ -16,6 +16,11 @@ export abstract class RegistrationFinishService { */ abstract getMasterPasswordPolicyOptsFromOrgInvite(): Promise; + /** + * Returns the route the user is redirected to after a successful login. + */ + abstract determineLoginSuccessRoute(): Promise; + /** * Finishes the registration process by creating a new user account. * diff --git a/libs/common/spec/fake-storage.service.ts b/libs/common/spec/fake-storage.service.ts index c6d989c5abf..1eae3dbfbe3 100644 --- a/libs/common/spec/fake-storage.service.ts +++ b/libs/common/spec/fake-storage.service.ts @@ -1,119 +1 @@ -import { MockProxy, mock } from "jest-mock-extended"; -import { Subject } from "rxjs"; - -import { - AbstractStorageService, - ObservableStorageService, - StorageUpdate, -} from "../src/platform/abstractions/storage.service"; -import { StorageOptions } from "../src/platform/models/domain/storage-options"; - -const INTERNAL_KEY = "__internal__"; - -export class FakeStorageService implements AbstractStorageService, ObservableStorageService { - private store: Record; - private updatesSubject = new Subject(); - private _valuesRequireDeserialization = false; - - /** - * Returns a mock of a {@see AbstractStorageService} for asserting the expected - * amount of calls. It is not recommended to use this to mock implementations as - * they are not respected. - */ - mock: MockProxy; - - constructor(initial?: Record) { - this.store = initial ?? {}; - this.mock = mock(); - } - - /** - * Updates the internal store for this fake implementation, this bypasses any mock calls - * or updates to the {@link updates$} observable. - * @param store - */ - internalUpdateStore(store: Record) { - this.store = store; - } - - get internalStore() { - return this.store; - } - - internalUpdateValuesRequireDeserialization(value: boolean) { - this._valuesRequireDeserialization = value; - } - - get valuesRequireDeserialization(): boolean { - return this._valuesRequireDeserialization; - } - - get updates$() { - return this.updatesSubject.asObservable(); - } - - get(key: string, options?: StorageOptions): Promise { - // 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.mock.get(key, options); - const value = this.store[key] as T; - return Promise.resolve(value); - } - has(key: string, options?: StorageOptions): Promise { - // 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.mock.has(key, options); - return Promise.resolve(this.store[key] != null); - } - async save(key: string, obj: T, options?: StorageOptions): Promise { - // These exceptions are copied from https://github.com/sindresorhus/conf/blob/608adb0c46fb1680ddbd9833043478367a64c120/source/index.ts#L193-L203 - // which is a library that is used by `ElectronStorageService`. We add them here to ensure that the behavior in our testing mirrors the real world. - if (typeof key !== "string" && typeof key !== "object") { - throw new TypeError( - `Expected \`key\` to be of type \`string\` or \`object\`, got ${typeof key}`, - ); - } - - // We don't throw this error because ElectronStorageService automatically detects this case - // and calls `delete()` instead of `set()`. - // if (typeof key !== "object" && obj === undefined) { - // throw new TypeError("Use `delete()` to clear values"); - // } - - if (this._containsReservedKey(key)) { - throw new TypeError( - `Please don't use the ${INTERNAL_KEY} key, as it's used to manage this module internal operations.`, - ); - } - - // 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.mock.save(key, obj, options); - this.store[key] = obj; - this.updatesSubject.next({ key: key, updateType: "save" }); - } - remove(key: string, options?: StorageOptions): Promise { - // 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.mock.remove(key, options); - delete this.store[key]; - this.updatesSubject.next({ key: key, updateType: "remove" }); - return Promise.resolve(); - } - - private _containsReservedKey(key: string | Partial): boolean { - if (typeof key === "object") { - const firsKey = Object.keys(key)[0]; - - if (firsKey === INTERNAL_KEY) { - return true; - } - } - - if (typeof key !== "string") { - return false; - } - - return false; - } -} +export { FakeStorageService } from "@bitwarden/storage-test-utils"; diff --git a/libs/common/spec/jest-sdk-client-factory.ts b/libs/common/spec/jest-sdk-client-factory.ts index ff120ccd787..8e5e1c9d3fc 100644 --- a/libs/common/spec/jest-sdk-client-factory.ts +++ b/libs/common/spec/jest-sdk-client-factory.ts @@ -1,9 +1,11 @@ -import { ClientSettings, LogLevel, BitwardenClient } from "@bitwarden/sdk-internal"; +import { BitwardenClient } from "@bitwarden/sdk-internal"; import { SdkClientFactory } from "../src/platform/abstractions/sdk/sdk-client-factory"; export class DefaultSdkClientFactory implements SdkClientFactory { - createSdkClient(settings?: ClientSettings, log_level?: LogLevel): Promise { + createSdkClient( + ...args: ConstructorParameters + ): Promise { throw new Error("Method not implemented."); } } diff --git a/libs/common/src/auth/utils/assert-non-nullish.util.ts b/libs/common/src/auth/utils/assert-non-nullish.util.ts new file mode 100644 index 00000000000..91fb8ef44b8 --- /dev/null +++ b/libs/common/src/auth/utils/assert-non-nullish.util.ts @@ -0,0 +1,45 @@ +/** + * Asserts that a value is non-nullish (not `null` or `undefined`); throws if value is nullish. + * + * @param val the value to check + * @param name the name of the value to include in the error message + * @param ctx context to optionally append to the error message + * @throws if the value is null or undefined + * + * @example + * + * ``` + * // `newPasswordHint` can have an empty string as a valid value, so we check non-nullish + * this.assertNonNullish( + * passwordInputResult.newPasswordHint, + * "newPasswordHint", + * "Could not set initial password." + * ); + * // Output error message: "newPasswordHint is null or undefined. Could not set initial password." + * ``` + * + * @remarks + * + * If you use this method repeatedly to check several values, it may help to assign any + * additional context (`ctx`) to a variable and pass it in to each call. This prevents the + * call from reformatting vertically via prettier in your text editor, taking up multiple lines. + * + * For example: + * ``` + * const ctx = "Could not set initial password."; + * + * this.assertNonNullish(valueOne, "valueOne", ctx); + * this.assertNonNullish(valueTwo, "valueTwo", ctx); + * this.assertNonNullish(valueThree, "valueThree", ctx); + * ``` + */ +export function assertNonNullish( + val: T, + name: string, + ctx?: string, +): asserts val is NonNullable { + if (val == null) { + // If context is provided, append it to the error message with a space before it. + throw new Error(`${name} is null or undefined.${ctx ? ` ${ctx}` : ""}`); + } +} diff --git a/libs/common/src/auth/utils/assert-truthy.util.ts b/libs/common/src/auth/utils/assert-truthy.util.ts new file mode 100644 index 00000000000..8e003186929 --- /dev/null +++ b/libs/common/src/auth/utils/assert-truthy.util.ts @@ -0,0 +1,46 @@ +/** + * Asserts that a value is truthy; throws if value is falsy. + * + * @param val the value to check + * @param name the name of the value to include in the error message + * @param ctx context to optionally append to the error message + * @throws if the value is falsy (`false`, `""`, `0`, `null`, `undefined`, `void`, or `NaN`) + * + * @example + * + * ``` + * this.assertTruthy( + * this.organizationId, + * "organizationId", + * "Could not set initial password." + * ); + * // Output error message: "organizationId is falsy. Could not set initial password." + * ``` + * + * @remarks + * + * If you use this method repeatedly to check several values, it may help to assign any + * additional context (`ctx`) to a variable and pass it in to each call. This prevents the + * call from reformatting vertically via prettier in your text editor, taking up multiple lines. + * + * For example: + * ``` + * const ctx = "Could not set initial password."; + * + * this.assertTruthy(valueOne, "valueOne", ctx); + * this.assertTruthy(valueTwo, "valueTwo", ctx); + * this.assertTruthy(valueThree, "valueThree", ctx); + */ +export function assertTruthy( + val: T, + name: string, + ctx?: string, +): asserts val is Exclude { + // Because `NaN` is a value (not a type) of type 'number', that means we cannot add + // it to the list of falsy values in the type assertion. Instead, we check for it + // separately at runtime. + if (!val || (typeof val === "number" && Number.isNaN(val))) { + // If context is provided, append it to the error message with a space before it. + throw new Error(`${name} is falsy.${ctx ? ` ${ctx}` : ""}`); + } +} diff --git a/libs/common/src/auth/utils/index.ts b/libs/common/src/auth/utils/index.ts new file mode 100644 index 00000000000..96bab53d3f9 --- /dev/null +++ b/libs/common/src/auth/utils/index.ts @@ -0,0 +1,2 @@ +export { assertTruthy } from "./assert-truthy.util"; +export { assertNonNullish } from "./assert-non-nullish.util"; diff --git a/libs/common/src/enums/device-type.enum.ts b/libs/common/src/enums/device-type.enum.ts index d5628536ff7..c462081140e 100644 --- a/libs/common/src/enums/device-type.enum.ts +++ b/libs/common/src/enums/device-type.enum.ts @@ -27,6 +27,7 @@ export enum DeviceType { WindowsCLI = 23, MacOsCLI = 24, LinuxCLI = 25, + DuckDuckGoBrowser = 26, } /** @@ -55,6 +56,7 @@ export const DeviceTypeMetadata: Record = { [DeviceType.IEBrowser]: { category: "webVault", platform: "IE" }, [DeviceType.SafariBrowser]: { category: "webVault", platform: "Safari" }, [DeviceType.VivaldiBrowser]: { category: "webVault", platform: "Vivaldi" }, + [DeviceType.DuckDuckGoBrowser]: { category: "webVault", platform: "DuckDuckGo" }, [DeviceType.UnknownBrowser]: { category: "webVault", platform: "Unknown" }, [DeviceType.WindowsDesktop]: { category: "desktop", platform: "Windows" }, [DeviceType.MacOsDesktop]: { category: "desktop", platform: "macOS" }, diff --git a/libs/common/src/enums/event-type.enum.ts b/libs/common/src/enums/event-type.enum.ts index 9efe49b1b0f..914dac00abb 100644 --- a/libs/common/src/enums/event-type.enum.ts +++ b/libs/common/src/enums/event-type.enum.ts @@ -90,4 +90,7 @@ export enum EventType { OrganizationDomain_NotVerified = 2003, Secret_Retrieved = 2100, + Secret_Created = 2101, + Secret_Edited = 2102, + Secret_Deleted = 2103, } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 55c96c2334c..68228b63bea 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -33,6 +33,7 @@ export enum FeatureFlag { PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships", PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup", UseOrganizationWarningsService = "use-organization-warnings-service", + AllowTrialLengthZero = "pm-20322-allow-trial-length-0", /* Data Insights and Reporting */ EnableRiskInsightsNotifications = "enable-risk-insights-notifications", @@ -55,6 +56,7 @@ export enum FeatureFlag { PM18520_UpdateDesktopCipherForm = "pm-18520-desktop-cipher-forms", EndUserNotifications = "pm-10609-end-user-notifications", RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy", + PM19315EndUserActivationMvp = "pm-19315-end-user-activation-mvp", /* Platform */ IpcChannelFramework = "ipc-channel-framework", @@ -99,6 +101,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.EndUserNotifications]: FALSE, [FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE, [FeatureFlag.RemoveCardItemTypePolicy]: FALSE, + [FeatureFlag.PM19315EndUserActivationMvp]: FALSE, /* Auth */ [FeatureFlag.PM16117_SetInitialPasswordRefactor]: FALSE, @@ -112,6 +115,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE, [FeatureFlag.PM19956_RequireProviderPaymentMethodDuringSetup]: FALSE, [FeatureFlag.UseOrganizationWarningsService]: FALSE, + [FeatureFlag.AllowTrialLengthZero]: FALSE, /* Key Management */ [FeatureFlag.PrivateKeyRegeneration]: FALSE, diff --git a/libs/common/src/enums/notification-type.enum.ts b/libs/common/src/enums/notification-type.enum.ts index 6d731253ce3..d362ffc841a 100644 --- a/libs/common/src/enums/notification-type.enum.ts +++ b/libs/common/src/enums/notification-type.enum.ts @@ -29,5 +29,5 @@ export enum NotificationType { Notification = 20, NotificationStatus = 21, - PendingSecurityTasks = 22, + RefreshSecurityTasks = 22, } diff --git a/libs/common/src/models/export/cipher.export.ts b/libs/common/src/models/export/cipher.export.ts index 7d0ef9e9c34..fa050375391 100644 --- a/libs/common/src/models/export/cipher.export.ts +++ b/libs/common/src/models/export/cipher.export.ts @@ -124,9 +124,9 @@ export class CipherExport { domain.passwordHistory = req.passwordHistory.map((ph) => PasswordHistoryExport.toDomain(ph)); } - domain.creationDate = req.creationDate; - domain.revisionDate = req.revisionDate; - domain.deletedDate = req.deletedDate; + domain.creationDate = req.creationDate ? new Date(req.creationDate) : null; + domain.revisionDate = req.revisionDate ? new Date(req.revisionDate) : null; + domain.deletedDate = req.deletedDate ? new Date(req.deletedDate) : null; return domain; } diff --git a/libs/common/src/models/export/password-history.export.ts b/libs/common/src/models/export/password-history.export.ts index ece7e331009..5382b9cb974 100644 --- a/libs/common/src/models/export/password-history.export.ts +++ b/libs/common/src/models/export/password-history.export.ts @@ -22,7 +22,7 @@ export class PasswordHistoryExport { static toDomain(req: PasswordHistoryExport, domain = new Password()) { domain.password = req.password != null ? new EncString(req.password) : null; - domain.lastUsedDate = req.lastUsedDate; + domain.lastUsedDate = req.lastUsedDate ? new Date(req.lastUsedDate) : null; return domain; } diff --git a/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts index 15655393362..fd3453198e6 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts @@ -70,7 +70,7 @@ export class Fido2AuthenticatorError extends Error { } export interface PublicKeyCredentialDescriptor { - id: Uint8Array; + id: ArrayBuffer; transports?: ("ble" | "hybrid" | "internal" | "nfc" | "usb")[]; type: "public-key"; } @@ -155,9 +155,9 @@ export interface Fido2AuthenticatorGetAssertionParams { export interface Fido2AuthenticatorGetAssertionResult { selectedCredential: { - id: Uint8Array; - userHandle?: Uint8Array; + id: ArrayBuffer; + userHandle?: ArrayBuffer; }; - authenticatorData: Uint8Array; - signature: Uint8Array; + authenticatorData: ArrayBuffer; + signature: ArrayBuffer; } diff --git a/libs/common/src/platform/abstractions/log.service.ts b/libs/common/src/platform/abstractions/log.service.ts index d77a4f69906..c540f1a2b8f 100644 --- a/libs/common/src/platform/abstractions/log.service.ts +++ b/libs/common/src/platform/abstractions/log.service.ts @@ -1,9 +1 @@ -import { LogLevelType } from "../enums/log-level-type.enum"; - -export abstract class LogService { - abstract debug(message?: any, ...optionalParams: any[]): void; - abstract info(message?: any, ...optionalParams: any[]): void; - abstract warning(message?: any, ...optionalParams: any[]): void; - abstract error(message?: any, ...optionalParams: any[]): void; - abstract write(level: LogLevelType, message?: any, ...optionalParams: any[]): void; -} +export { LogService } from "@bitwarden/logging"; diff --git a/libs/common/src/platform/abstractions/platform-utils.service.ts b/libs/common/src/platform/abstractions/platform-utils.service.ts index fa0fc8f2501..7586da5a564 100644 --- a/libs/common/src/platform/abstractions/platform-utils.service.ts +++ b/libs/common/src/platform/abstractions/platform-utils.service.ts @@ -28,6 +28,15 @@ export abstract class PlatformUtilsService { abstract getApplicationVersionNumber(): Promise; abstract supportsWebAuthn(win: Window): boolean; abstract supportsDuo(): boolean; + /** + * Returns true if the device supports autofill functionality + */ + abstract supportsAutofill(): boolean; + /** + * Returns true if the device supports native file downloads without + * the need for `target="_blank"` + */ + abstract supportsFileDownloads(): boolean; /** * @deprecated use `@bitwarden/components/ToastService.showToast` instead * diff --git a/libs/common/src/platform/enums/log-level-type.enum.ts b/libs/common/src/platform/enums/log-level-type.enum.ts index b5f84467d6e..024c71c9f97 100644 --- a/libs/common/src/platform/enums/log-level-type.enum.ts +++ b/libs/common/src/platform/enums/log-level-type.enum.ts @@ -1,8 +1 @@ -// FIXME: update to use a const object instead of a typescript enum -// eslint-disable-next-line @bitwarden/platform/no-enums -export enum LogLevelType { - Debug, - Info, - Warning, - Error, -} +export { LogLevel as LogLevelType } from "@bitwarden/logging"; diff --git a/libs/common/src/platform/notifications/internal/signalr-connection.service.ts b/libs/common/src/platform/notifications/internal/signalr-connection.service.ts index 8bea98cb506..58d6311c668 100644 --- a/libs/common/src/platform/notifications/internal/signalr-connection.service.ts +++ b/libs/common/src/platform/notifications/internal/signalr-connection.service.ts @@ -31,22 +31,35 @@ export type TimeoutManager = { class SignalRLogger implements ILogger { constructor(private readonly logService: LogService) {} + redactMessage(message: string): string { + const ACCESS_TOKEN_TEXT = "access_token="; + // Redact the access token from the logs if it exists. + const accessTokenIndex = message.indexOf(ACCESS_TOKEN_TEXT); + if (accessTokenIndex !== -1) { + return message.substring(0, accessTokenIndex + ACCESS_TOKEN_TEXT.length) + "[REDACTED]"; + } + + return message; + } + log(logLevel: LogLevel, message: string): void { + const redactedMessage = `[SignalR] ${this.redactMessage(message)}`; + switch (logLevel) { case LogLevel.Critical: - this.logService.error(message); + this.logService.error(redactedMessage); break; case LogLevel.Error: - this.logService.error(message); + this.logService.error(redactedMessage); break; case LogLevel.Warning: - this.logService.warning(message); + this.logService.warning(redactedMessage); break; case LogLevel.Information: - this.logService.info(message); + this.logService.info(redactedMessage); break; case LogLevel.Debug: - this.logService.debug(message); + this.logService.debug(redactedMessage); break; } } diff --git a/libs/common/src/platform/services/console-log.service.spec.ts b/libs/common/src/platform/services/console-log.service.spec.ts index 508ca4eb327..e73aed5f3b5 100644 --- a/libs/common/src/platform/services/console-log.service.spec.ts +++ b/libs/common/src/platform/services/console-log.service.spec.ts @@ -1,6 +1,6 @@ -import { interceptConsole, restoreConsole } from "../../../spec"; +import { ConsoleLogService } from "@bitwarden/logging"; -import { ConsoleLogService } from "./console-log.service"; +import { interceptConsole, restoreConsole } from "../../../spec"; describe("ConsoleLogService", () => { const error = new Error("this is an error"); diff --git a/libs/common/src/platform/services/console-log.service.ts b/libs/common/src/platform/services/console-log.service.ts index cb6554e2aa2..6d55614757b 100644 --- a/libs/common/src/platform/services/console-log.service.ts +++ b/libs/common/src/platform/services/console-log.service.ts @@ -1,59 +1 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { LogService as LogServiceAbstraction } from "../abstractions/log.service"; -import { LogLevelType } from "../enums/log-level-type.enum"; - -export class ConsoleLogService implements LogServiceAbstraction { - protected timersMap: Map = new Map(); - - constructor( - protected isDev: boolean, - protected filter: (level: LogLevelType) => boolean = null, - ) {} - - debug(message?: any, ...optionalParams: any[]) { - if (!this.isDev) { - return; - } - this.write(LogLevelType.Debug, message, ...optionalParams); - } - - info(message?: any, ...optionalParams: any[]) { - this.write(LogLevelType.Info, message, ...optionalParams); - } - - warning(message?: any, ...optionalParams: any[]) { - this.write(LogLevelType.Warning, message, ...optionalParams); - } - - error(message?: any, ...optionalParams: any[]) { - this.write(LogLevelType.Error, message, ...optionalParams); - } - - write(level: LogLevelType, message?: any, ...optionalParams: any[]) { - if (this.filter != null && this.filter(level)) { - return; - } - - switch (level) { - case LogLevelType.Debug: - // eslint-disable-next-line - console.log(message, ...optionalParams); - break; - case LogLevelType.Info: - // eslint-disable-next-line - console.log(message, ...optionalParams); - break; - case LogLevelType.Warning: - // eslint-disable-next-line - console.warn(message, ...optionalParams); - break; - case LogLevelType.Error: - // eslint-disable-next-line - console.error(message, ...optionalParams); - break; - default: - break; - } - } -} +export { ConsoleLogService } from "@bitwarden/logging"; diff --git a/libs/common/src/platform/services/fido2/credential-id-utils.spec.ts b/libs/common/src/platform/services/fido2/credential-id-utils.spec.ts index 76e068ac01c..1f2217ccd63 100644 --- a/libs/common/src/platform/services/fido2/credential-id-utils.spec.ts +++ b/libs/common/src/platform/services/fido2/credential-id-utils.spec.ts @@ -9,7 +9,7 @@ describe("credential-id-utils", () => { new Uint8Array([ 0x08, 0xd7, 0x0b, 0x74, 0xe9, 0xf5, 0x45, 0x22, 0xa4, 0x25, 0xe5, 0xdc, 0xd4, 0x01, 0x07, 0xe7, - ]), + ]).buffer, ); }); @@ -20,7 +20,7 @@ describe("credential-id-utils", () => { new Uint8Array([ 0x08, 0xd7, 0x0b, 0x74, 0xe9, 0xf5, 0x45, 0x22, 0xa4, 0x25, 0xe5, 0xdc, 0xd4, 0x01, 0x07, 0xe7, - ]), + ]).buffer, ); }); diff --git a/libs/common/src/platform/services/fido2/credential-id-utils.ts b/libs/common/src/platform/services/fido2/credential-id-utils.ts index 685669f0da3..08ea33114f5 100644 --- a/libs/common/src/platform/services/fido2/credential-id-utils.ts +++ b/libs/common/src/platform/services/fido2/credential-id-utils.ts @@ -3,13 +3,13 @@ import { Fido2Utils } from "./fido2-utils"; import { guidToRawFormat } from "./guid-utils"; -export function parseCredentialId(encodedCredentialId: string): Uint8Array { +export function parseCredentialId(encodedCredentialId: string): ArrayBuffer { try { if (encodedCredentialId.startsWith("b64.")) { return Fido2Utils.stringToBuffer(encodedCredentialId.slice(4)); } - return guidToRawFormat(encodedCredentialId); + return guidToRawFormat(encodedCredentialId).buffer; } catch { return undefined; } @@ -18,13 +18,16 @@ export function parseCredentialId(encodedCredentialId: string): Uint8Array { /** * Compares two credential IDs for equality. */ -export function compareCredentialIds(a: Uint8Array, b: Uint8Array): boolean { - if (a.length !== b.length) { +export function compareCredentialIds(a: ArrayBuffer, b: ArrayBuffer): boolean { + if (a.byteLength !== b.byteLength) { return false; } - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) { + const viewA = new Uint8Array(a); + const viewB = new Uint8Array(b); + + for (let i = 0; i < viewA.length; i++) { + if (viewA[i] !== viewB[i]) { return false; } } diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts index bac1b218657..e560a77cc2e 100644 --- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts +++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts @@ -514,7 +514,7 @@ async function getPrivateKeyFromFido2Credential( const keyBuffer = Fido2Utils.stringToBuffer(fido2Credential.keyValue); return await crypto.subtle.importKey( "pkcs8", - keyBuffer, + new Uint8Array(keyBuffer), { name: fido2Credential.keyAlgorithm, namedCurve: fido2Credential.keyCurve, diff --git a/libs/common/src/platform/services/fido2/fido2-client.service.ts b/libs/common/src/platform/services/fido2/fido2-client.service.ts index 5d5f2a879cb..431585441a7 100644 --- a/libs/common/src/platform/services/fido2/fido2-client.service.ts +++ b/libs/common/src/platform/services/fido2/fido2-client.service.ts @@ -127,9 +127,9 @@ export class Fido2ClientService } const userId = Fido2Utils.stringToBuffer(params.user.id); - if (userId.length < 1 || userId.length > 64) { + if (userId.byteLength < 1 || userId.byteLength > 64) { this.logService?.warning( - `[Fido2Client] Invalid 'user.id' length: ${params.user.id} (${userId.length})`, + `[Fido2Client] Invalid 'user.id' length: ${params.user.id} (${userId.byteLength})`, ); throw new TypeError("Invalid 'user.id' length"); } diff --git a/libs/common/src/platform/services/fido2/fido2-utils.ts b/libs/common/src/platform/services/fido2/fido2-utils.ts index 6413eeade04..99e260f4a53 100644 --- a/libs/common/src/platform/services/fido2/fido2-utils.ts +++ b/libs/common/src/platform/services/fido2/fido2-utils.ts @@ -47,8 +47,8 @@ export class Fido2Utils { .replace(/=/g, ""); } - static stringToBuffer(str: string): Uint8Array { - return Fido2Utils.fromB64ToArray(Fido2Utils.fromUrlB64ToB64(str)); + static stringToBuffer(str: string): ArrayBuffer { + return Fido2Utils.fromB64ToArray(Fido2Utils.fromUrlB64ToB64(str)).buffer; } static bufferSourceToUint8Array(bufferSource: BufferSource): Uint8Array { diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.ts b/libs/common/src/platform/services/sdk/default-sdk.service.ts index e874dae3461..a0d00b5eef6 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.ts @@ -21,6 +21,7 @@ import { BitwardenClient, ClientSettings, DeviceType as SdkDeviceType, + TokenProvider, } from "@bitwarden/sdk-internal"; import { EncryptedOrganizationKeyData } from "../../../admin-console/models/data/encrypted-organization-key.data"; @@ -41,6 +42,17 @@ import { EncryptedString } from "../../models/domain/enc-string"; // blocking the creation of an internal client for that user. const UnsetClient = Symbol("UnsetClient"); +/** + * A token provider that exposes the access token to the SDK. + */ +class JsTokenProvider implements TokenProvider { + constructor() {} + + async get_access_token(): Promise { + return undefined; + } +} + export class DefaultSdkService implements SdkService { private sdkClientOverrides = new BehaviorSubject<{ [userId: UserId]: Rc | typeof UnsetClient; @@ -51,7 +63,7 @@ export class DefaultSdkService implements SdkService { concatMap(async (env) => { await SdkLoadService.Ready; const settings = this.toSettings(env); - return await this.sdkClientFactory.createSdkClient(settings); + return await this.sdkClientFactory.createSdkClient(new JsTokenProvider(), settings); }), shareReplay({ refCount: true, bufferSize: 1 }), ); @@ -151,7 +163,10 @@ export class DefaultSdkService implements SdkService { } const settings = this.toSettings(env); - const client = await this.sdkClientFactory.createSdkClient(settings); + const client = await this.sdkClientFactory.createSdkClient( + new JsTokenProvider(), + settings, + ); await this.initializeClient( userId, diff --git a/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts b/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts index b73415d6b79..1cb1453a509 100644 --- a/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts +++ b/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts @@ -6,12 +6,13 @@ import { any, mock } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom, map, of, timeout } from "rxjs"; import { Jsonify } from "type-fest"; +import { StorageServiceProvider } from "@bitwarden/storage-core"; + import { awaitAsync, trackEmissions } from "../../../../spec"; import { FakeStorageService } from "../../../../spec/fake-storage.service"; import { Account } from "../../../auth/abstractions/account.service"; import { UserId } from "../../../types/guid"; import { LogService } from "../../abstractions/log.service"; -import { StorageServiceProvider } from "../../services/storage-service.provider"; import { StateDefinition } from "../state-definition"; import { StateEventRegistrarService } from "../state-event-registrar.service"; import { UserKeyDefinition } from "../user-key-definition"; diff --git a/libs/common/src/platform/state/implementations/default-global-state.provider.ts b/libs/common/src/platform/state/implementations/default-global-state.provider.ts index 18e9c21e75e..bd0cfc1dc9a 100644 --- a/libs/common/src/platform/state/implementations/default-global-state.provider.ts +++ b/libs/common/src/platform/state/implementations/default-global-state.provider.ts @@ -1,7 +1,8 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { StorageServiceProvider } from "@bitwarden/storage-core"; + import { LogService } from "../../abstractions/log.service"; -import { StorageServiceProvider } from "../../services/storage-service.provider"; import { GlobalState } from "../global-state"; import { GlobalStateProvider } from "../global-state.provider"; import { KeyDefinition } from "../key-definition"; diff --git a/libs/common/src/platform/state/implementations/default-global-state.ts b/libs/common/src/platform/state/implementations/default-global-state.ts index c88e9303c8e..a06eb23e010 100644 --- a/libs/common/src/platform/state/implementations/default-global-state.ts +++ b/libs/common/src/platform/state/implementations/default-global-state.ts @@ -1,8 +1,6 @@ +import { AbstractStorageService, ObservableStorageService } from "@bitwarden/storage-core"; + import { LogService } from "../../abstractions/log.service"; -import { - AbstractStorageService, - ObservableStorageService, -} from "../../abstractions/storage.service"; import { GlobalState } from "../global-state"; import { KeyDefinition, globalKeyBuilder } from "../key-definition"; diff --git a/libs/common/src/platform/state/implementations/default-single-user-state.provider.ts b/libs/common/src/platform/state/implementations/default-single-user-state.provider.ts index 54e268e0b69..bef56ad2309 100644 --- a/libs/common/src/platform/state/implementations/default-single-user-state.provider.ts +++ b/libs/common/src/platform/state/implementations/default-single-user-state.provider.ts @@ -1,8 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { StorageServiceProvider } from "@bitwarden/storage-core"; + import { UserId } from "../../../types/guid"; import { LogService } from "../../abstractions/log.service"; -import { StorageServiceProvider } from "../../services/storage-service.provider"; import { StateEventRegistrarService } from "../state-event-registrar.service"; import { UserKeyDefinition } from "../user-key-definition"; import { SingleUserState } from "../user-state"; diff --git a/libs/common/src/platform/state/implementations/default-single-user-state.ts b/libs/common/src/platform/state/implementations/default-single-user-state.ts index 1dafd3aecad..db776c3d11d 100644 --- a/libs/common/src/platform/state/implementations/default-single-user-state.ts +++ b/libs/common/src/platform/state/implementations/default-single-user-state.ts @@ -1,11 +1,9 @@ import { Observable, combineLatest, of } from "rxjs"; +import { AbstractStorageService, ObservableStorageService } from "@bitwarden/storage-core"; + import { UserId } from "../../../types/guid"; import { LogService } from "../../abstractions/log.service"; -import { - AbstractStorageService, - ObservableStorageService, -} from "../../abstractions/storage.service"; import { StateEventRegistrarService } from "../state-event-registrar.service"; import { UserKeyDefinition } from "../user-key-definition"; import { CombinedState, SingleUserState } from "../user-state"; diff --git a/libs/common/src/platform/state/implementations/specific-state.provider.spec.ts b/libs/common/src/platform/state/implementations/specific-state.provider.spec.ts index 1b5a36445c9..6674c2577d7 100644 --- a/libs/common/src/platform/state/implementations/specific-state.provider.spec.ts +++ b/libs/common/src/platform/state/implementations/specific-state.provider.spec.ts @@ -1,10 +1,11 @@ import { mock } from "jest-mock-extended"; +import { StorageServiceProvider } from "@bitwarden/storage-core"; + import { mockAccountServiceWith } from "../../../../spec/fake-account-service"; import { FakeStorageService } from "../../../../spec/fake-storage.service"; import { UserId } from "../../../types/guid"; import { LogService } from "../../abstractions/log.service"; -import { StorageServiceProvider } from "../../services/storage-service.provider"; import { KeyDefinition } from "../key-definition"; import { StateDefinition } from "../state-definition"; import { StateEventRegistrarService } from "../state-event-registrar.service"; diff --git a/libs/common/src/platform/state/implementations/state-base.ts b/libs/common/src/platform/state/implementations/state-base.ts index 578720a2281..03140e1fe1f 100644 --- a/libs/common/src/platform/state/implementations/state-base.ts +++ b/libs/common/src/platform/state/implementations/state-base.ts @@ -15,12 +15,10 @@ import { } from "rxjs"; import { Jsonify } from "type-fest"; +import { AbstractStorageService, ObservableStorageService } from "@bitwarden/storage-core"; + import { StorageKey } from "../../../types/state"; import { LogService } from "../../abstractions/log.service"; -import { - AbstractStorageService, - ObservableStorageService, -} from "../../abstractions/storage.service"; import { DebugOptions } from "../key-definition"; import { populateOptionsWithDefault, StateUpdateOptions } from "../state-update-options"; diff --git a/libs/common/src/platform/state/implementations/util.ts b/libs/common/src/platform/state/implementations/util.ts index 0a9d76f6da5..14b11f6b553 100644 --- a/libs/common/src/platform/state/implementations/util.ts +++ b/libs/common/src/platform/state/implementations/util.ts @@ -1,6 +1,6 @@ import { Jsonify } from "type-fest"; -import { AbstractStorageService } from "../../abstractions/storage.service"; +import { AbstractStorageService } from "@bitwarden/storage-core"; export async function getStoredValue( key: string, diff --git a/libs/common/src/platform/state/state-event-registrar.service.spec.ts b/libs/common/src/platform/state/state-event-registrar.service.spec.ts index 2fae985033b..b022e2ce413 100644 --- a/libs/common/src/platform/state/state-event-registrar.service.spec.ts +++ b/libs/common/src/platform/state/state-event-registrar.service.spec.ts @@ -1,8 +1,12 @@ import { mock } from "jest-mock-extended"; +import { + AbstractStorageService, + ObservableStorageService, + StorageServiceProvider, +} from "@bitwarden/storage-core"; + import { FakeGlobalStateProvider } from "../../../spec"; -import { AbstractStorageService, ObservableStorageService } from "../abstractions/storage.service"; -import { StorageServiceProvider } from "../services/storage-service.provider"; import { StateDefinition } from "./state-definition"; import { STATE_LOCK_EVENT, StateEventRegistrarService } from "./state-event-registrar.service"; diff --git a/libs/common/src/platform/state/state-event-runner.service.spec.ts b/libs/common/src/platform/state/state-event-runner.service.spec.ts index 1c98099a518..4aef3d8516c 100644 --- a/libs/common/src/platform/state/state-event-runner.service.spec.ts +++ b/libs/common/src/platform/state/state-event-runner.service.spec.ts @@ -1,9 +1,13 @@ import { mock } from "jest-mock-extended"; +import { + AbstractStorageService, + ObservableStorageService, + StorageServiceProvider, +} from "@bitwarden/storage-core"; + import { FakeGlobalStateProvider } from "../../../spec"; import { UserId } from "../../types/guid"; -import { AbstractStorageService, ObservableStorageService } from "../abstractions/storage.service"; -import { StorageServiceProvider } from "../services/storage-service.provider"; import { STATE_LOCK_EVENT } from "./state-event-registrar.service"; import { StateEventRunnerService } from "./state-event-runner.service"; diff --git a/libs/common/src/platform/state/state-event-runner.service.ts b/libs/common/src/platform/state/state-event-runner.service.ts index f24c50f86d6..9e6f8f214e1 100644 --- a/libs/common/src/platform/state/state-event-runner.service.ts +++ b/libs/common/src/platform/state/state-event-runner.service.ts @@ -2,8 +2,9 @@ // @ts-strict-ignore import { firstValueFrom } from "rxjs"; +import { StorageServiceProvider } from "@bitwarden/storage-core"; + import { UserId } from "../../types/guid"; -import { StorageServiceProvider } from "../services/storage-service.provider"; import { GlobalState } from "./global-state"; import { GlobalStateProvider } from "./global-state.provider"; diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index ca6cd6570a4..62d300fc029 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -97,7 +97,7 @@ import { PaymentResponse } from "../billing/models/response/payment.response"; import { PlanResponse } from "../billing/models/response/plan.response"; import { SubscriptionResponse } from "../billing/models/response/subscription.response"; import { TaxInfoResponse } from "../billing/models/response/tax-info.response"; -import { DeviceType } from "../enums"; +import { ClientType, DeviceType } from "../enums"; import { KeyConnectorUserKeyRequest } from "../key-management/key-connector/models/key-connector-user-key.request"; import { SetKeyConnectorKeyRequest } from "../key-management/key-connector/models/set-key-connector-key.request"; import { VaultTimeoutSettingsService } from "../key-management/vault-timeout"; @@ -154,8 +154,6 @@ export type HttpOperations = { export class ApiService implements ApiServiceAbstraction { private device: DeviceType; private deviceType: string; - private isWebClient = false; - private isDesktopClient = false; private refreshTokenPromise: Promise | undefined; /** @@ -178,22 +176,6 @@ export class ApiService implements ApiServiceAbstraction { ) { this.device = platformUtilsService.getDevice(); this.deviceType = this.device.toString(); - this.isWebClient = - this.device === DeviceType.IEBrowser || - this.device === DeviceType.ChromeBrowser || - this.device === DeviceType.EdgeBrowser || - this.device === DeviceType.FirefoxBrowser || - this.device === DeviceType.OperaBrowser || - this.device === DeviceType.SafariBrowser || - this.device === DeviceType.UnknownBrowser || - this.device === DeviceType.VivaldiBrowser; - this.isDesktopClient = - this.device === DeviceType.WindowsDesktop || - this.device === DeviceType.MacOsDesktop || - this.device === DeviceType.LinuxDesktop || - this.device === DeviceType.WindowsCLI || - this.device === DeviceType.MacOsCLI || - this.device === DeviceType.LinuxCLI; } // Auth APIs @@ -838,7 +820,9 @@ export class ApiService implements ApiServiceAbstraction { // Sync APIs async getSync(): Promise { - const path = this.isDesktopClient || this.isWebClient ? "/sync?excludeDomains=true" : "/sync"; + const path = !this.platformUtilsService.supportsAutofill() + ? "/sync?excludeDomains=true" + : "/sync"; const r = await this.send("GET", path, null, true, true); return new SyncResponse(r); } @@ -1875,7 +1859,7 @@ export class ApiService implements ApiServiceAbstraction { private async getCredentials(): Promise { const env = await firstValueFrom(this.environmentService.environment$); - if (!this.isWebClient || env.hasBaseUrl()) { + if (this.platformUtilsService.getClientType() !== ClientType.Web || env.hasBaseUrl()) { return "include"; } return undefined; diff --git a/libs/common/src/types/guid.ts b/libs/common/src/types/guid.ts index bf891b55691..5edd34e4fc5 100644 --- a/libs/common/src/types/guid.ts +++ b/libs/common/src/types/guid.ts @@ -2,7 +2,9 @@ import { Opaque } from "type-fest"; export type Guid = Opaque; -export type UserId = Opaque; +// Convenience re-export of UserId from it's original location, any library that +// wants to be lower level than common should instead import it from user-core. +export { UserId } from "@bitwarden/user-core"; export type OrganizationId = Opaque; export type CollectionId = Opaque; export type ProviderId = Opaque; diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts index e6d11a82b69..9cc9226cd46 100644 --- a/libs/common/src/vault/models/domain/cipher.ts +++ b/libs/common/src/vault/models/domain/cipher.ts @@ -353,14 +353,14 @@ export class Cipher extends Domain implements Decryptable { type: this.type, favorite: this.favorite ?? false, organizationUseTotp: this.organizationUseTotp ?? false, - edit: this.edit, + edit: this.edit ?? true, permissions: this.permissions ? { delete: this.permissions.delete, restore: this.permissions.restore, } : undefined, - viewPassword: this.viewPassword, + viewPassword: this.viewPassword ?? true, localData: this.localData ? { lastUsedDate: this.localData.lastUsedDate diff --git a/libs/common/src/vault/models/view/attachment.view.spec.ts b/libs/common/src/vault/models/view/attachment.view.spec.ts index 8ae836e1265..b24d251d246 100644 --- a/libs/common/src/vault/models/view/attachment.view.spec.ts +++ b/libs/common/src/vault/models/view/attachment.view.spec.ts @@ -26,6 +26,8 @@ describe("AttachmentView", () => { }); it("should return an AttachmentView from an SdkAttachmentView", () => { + jest.spyOn(SymmetricCryptoKey, "fromString").mockReturnValue("mockKey" as any); + const sdkAttachmentView = { id: "id", url: "url", @@ -33,6 +35,7 @@ describe("AttachmentView", () => { sizeName: "sizeName", fileName: "fileName", key: "encKeyB64_fromString", + decryptedKey: "decryptedKey_B64", } as SdkAttachmentView; const result = AttachmentView.fromSdkAttachmentView(sdkAttachmentView); @@ -43,14 +46,20 @@ describe("AttachmentView", () => { size: "size", sizeName: "sizeName", fileName: "fileName", - key: null, + key: "mockKey", encryptedKey: new EncString(sdkAttachmentView.key as string), }); + + expect(SymmetricCryptoKey.fromString).toHaveBeenCalledWith("decryptedKey_B64"); }); }); describe("toSdkAttachmentView", () => { it("should convert AttachmentView to SdkAttachmentView", () => { + const mockKey = { + toBase64: jest.fn().mockReturnValue("keyB64"), + } as any; + const attachmentView = new AttachmentView(); attachmentView.id = "id"; attachmentView.url = "url"; @@ -58,8 +67,10 @@ describe("AttachmentView", () => { attachmentView.sizeName = "sizeName"; attachmentView.fileName = "fileName"; attachmentView.encryptedKey = new EncString("encKeyB64"); + attachmentView.key = mockKey; const result = attachmentView.toSdkAttachmentView(); + expect(result).toEqual({ id: "id", url: "url", @@ -67,6 +78,7 @@ describe("AttachmentView", () => { sizeName: "sizeName", fileName: "fileName", key: "encKeyB64", + decryptedKey: "keyB64", }); }); }); diff --git a/libs/common/src/vault/models/view/attachment.view.ts b/libs/common/src/vault/models/view/attachment.view.ts index 2ef4280d97a..57a1deaedb9 100644 --- a/libs/common/src/vault/models/view/attachment.view.ts +++ b/libs/common/src/vault/models/view/attachment.view.ts @@ -59,6 +59,8 @@ export class AttachmentView implements View { sizeName: this.sizeName, fileName: this.fileName, key: this.encryptedKey?.toJSON(), + // TODO: PM-23005 - Temporary field, should be removed when encrypted migration is complete + decryptedKey: this.key ? this.key.toBase64() : null, }; } @@ -76,6 +78,8 @@ export class AttachmentView implements View { view.size = obj.size ?? null; view.sizeName = obj.sizeName ?? null; view.fileName = obj.fileName ?? null; + // TODO: PM-23005 - Temporary field, should be removed when encrypted migration is complete + view.key = obj.key ? SymmetricCryptoKey.fromString(obj.decryptedKey) : null; view.encryptedKey = new EncString(obj.key); return view; diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index b4f79b2467e..a1727fd7a1d 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -383,7 +383,7 @@ export class CipherService implements CipherServiceAbstraction { const decCiphers = await this.getDecryptedCiphers(userId); if (decCiphers != null && decCiphers.length !== 0) { await this.reindexCiphers(userId); - return await this.getDecryptedCiphers(userId); + return decCiphers; } const decrypted = await this.decryptCiphers(await this.getAll(userId), userId); diff --git a/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts b/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts index c0b3d3be85f..4d05a5197fb 100644 --- a/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts +++ b/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts @@ -7,7 +7,7 @@ import { CipherType as SdkCipherType, CipherView as SdkCipherView, CipherListView, - Attachment as SdkAttachment, + AttachmentView as SdkAttachmentView, } from "@bitwarden/sdk-internal"; import { mockEnc } from "../../../spec"; @@ -311,7 +311,9 @@ describe("DefaultCipherEncryptionService", () => { const expectedDecryptedContent = new Uint8Array([5, 6, 7, 8]); jest.spyOn(cipher, "toSdkCipher").mockReturnValue({ id: "id" } as SdkCipher); - jest.spyOn(attachment, "toSdkAttachmentView").mockReturnValue({ id: "a1" } as SdkAttachment); + jest + .spyOn(attachment, "toSdkAttachmentView") + .mockReturnValue({ id: "a1" } as SdkAttachmentView); mockSdkClient.vault().attachments().decrypt_buffer.mockReturnValue(expectedDecryptedContent); const result = await cipherEncryptionService.decryptAttachmentContent( diff --git a/libs/common/src/vault/services/restricted-item-types.service.ts b/libs/common/src/vault/services/restricted-item-types.service.ts index 7ec70831b22..6b848e6626b 100644 --- a/libs/common/src/vault/services/restricted-item-types.service.ts +++ b/libs/common/src/vault/services/restricted-item-types.service.ts @@ -91,7 +91,6 @@ export class RestrictedItemTypesService { * Restriction logic: * - If cipher type is not restricted by any org → allowed * - If cipher belongs to an org that allows this type → allowed - * - If cipher is personal vault and any org allows this type → allowed * - Otherwise → restricted */ isCipherRestricted(cipher: CipherLike, restrictedTypes: RestrictedCipherType[]): boolean { @@ -108,8 +107,8 @@ export class RestrictedItemTypesService { return !restriction.allowViewOrgIds.includes(cipher.organizationId); } - // For personal vault ciphers: restricted only if NO organizations allow this type - return restriction.allowViewOrgIds.length === 0; + // Cipher is restricted by at least one organization, restrict it + return true; } /** diff --git a/libs/common/src/vault/tasks/services/default-task.service.spec.ts b/libs/common/src/vault/tasks/services/default-task.service.spec.ts index d90889cf113..a1f9872266e 100644 --- a/libs/common/src/vault/tasks/services/default-task.service.spec.ts +++ b/libs/common/src/vault/tasks/services/default-task.service.spec.ts @@ -365,7 +365,7 @@ describe("Default task service", () => { const subscription = service.listenForTaskNotifications(); const notification = { - type: NotificationType.PendingSecurityTasks, + type: NotificationType.RefreshSecurityTasks, } as NotificationResponse; mockNotifications$.next([notification, userId]); @@ -390,7 +390,7 @@ describe("Default task service", () => { const subscription = service.listenForTaskNotifications(); const notification = { - type: NotificationType.PendingSecurityTasks, + type: NotificationType.RefreshSecurityTasks, } as NotificationResponse; mockNotifications$.next([notification, "other-user-id" as UserId]); diff --git a/libs/common/src/vault/tasks/services/default-task.service.ts b/libs/common/src/vault/tasks/services/default-task.service.ts index 5858ba832d5..35a7561d63d 100644 --- a/libs/common/src/vault/tasks/services/default-task.service.ts +++ b/libs/common/src/vault/tasks/services/default-task.service.ts @@ -152,7 +152,7 @@ export class DefaultTaskService implements TaskService { return this.notificationService.notifications$.pipe( filter( ([notification, userId]) => - notification.type === NotificationType.PendingSecurityTasks && + notification.type === NotificationType.RefreshSecurityTasks && filterByUserIds.includes(userId), ), map(([, userId]) => userId), diff --git a/libs/common/src/vault/utils/get-web-store-url.ts b/libs/common/src/vault/utils/get-web-store-url.ts new file mode 100644 index 00000000000..87698d65de2 --- /dev/null +++ b/libs/common/src/vault/utils/get-web-store-url.ts @@ -0,0 +1,22 @@ +import { DeviceType } from "@bitwarden/common/enums"; + +/** + * Returns the web store URL for the Bitwarden browser extension based on the device type. + * @defaults Bitwarden download page + */ +export const getWebStoreUrl = (deviceType: DeviceType): string => { + switch (deviceType) { + case DeviceType.ChromeBrowser: + return "https://chromewebstore.google.com/detail/bitwarden-password-manage/nngceckbapebfimnlniiiahkandclblb"; + case DeviceType.FirefoxBrowser: + return "https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/"; + case DeviceType.SafariBrowser: + return "https://apps.apple.com/us/app/bitwarden/id1352778147?mt=12"; + case DeviceType.OperaBrowser: + return "https://addons.opera.com/extensions/details/bitwarden-free-password-manager/"; + case DeviceType.EdgeBrowser: + return "https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh"; + default: + return "https://bitwarden.com/download/#downloads-web-browser"; + } +}; diff --git a/libs/components/src/anon-layout/anon-layout-wrapper.component.html b/libs/components/src/anon-layout/anon-layout-wrapper.component.html index 2cba7ca7783..3509e4dcdb0 100644 --- a/libs/components/src/anon-layout/anon-layout-wrapper.component.html +++ b/libs/components/src/anon-layout/anon-layout-wrapper.component.html @@ -4,8 +4,8 @@ [icon]="pageIcon" [showReadonlyHostname]="showReadonlyHostname" [maxWidth]="maxWidth" - [titleAreaMaxWidth]="titleAreaMaxWidth" [hideCardWrapper]="hideCardWrapper" + [hideIcon]="hideIcon" > diff --git a/libs/components/src/anon-layout/anon-layout-wrapper.component.ts b/libs/components/src/anon-layout/anon-layout-wrapper.component.ts index ea6a518f70d..ac192645ee6 100644 --- a/libs/components/src/anon-layout/anon-layout-wrapper.component.ts +++ b/libs/components/src/anon-layout/anon-layout-wrapper.component.ts @@ -10,7 +10,7 @@ import { Translation } from "../dialog"; import { Icon } from "../icon"; import { AnonLayoutWrapperDataService } from "./anon-layout-wrapper-data.service"; -import { AnonLayoutComponent } from "./anon-layout.component"; +import { AnonLayoutComponent, AnonLayoutMaxWidth } from "./anon-layout.component"; export interface AnonLayoutWrapperData { /** @@ -29,6 +29,10 @@ export interface AnonLayoutWrapperData { * The optional icon to display on the page. */ pageIcon?: Icon | null; + /** + * Hides the default Bitwarden shield icon. + */ + hideIcon?: boolean; /** * Optional flag to either show the optional environment selector (false) or just a readonly hostname (true). */ @@ -36,11 +40,7 @@ export interface AnonLayoutWrapperData { /** * Optional flag to set the max-width of the page. Defaults to 'md' if not provided. */ - maxWidth?: "md" | "3xl"; - /** - * Optional flag to set the max-width of the title area. Defaults to null if not provided. - */ - titleAreaMaxWidth?: "md"; + maxWidth?: AnonLayoutMaxWidth; /** * Hide the card that wraps the default content. Defaults to false. */ @@ -58,9 +58,9 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { protected pageSubtitle: string; protected pageIcon: Icon; protected showReadonlyHostname: boolean; - protected maxWidth: "md" | "3xl"; - protected titleAreaMaxWidth: "md"; + protected maxWidth: AnonLayoutMaxWidth; protected hideCardWrapper: boolean; + protected hideIcon: boolean = false; constructor( private router: Router, @@ -109,9 +109,12 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { this.pageIcon = firstChildRouteData["pageIcon"]; } + if (firstChildRouteData["hideIcon"] !== undefined) { + this.hideIcon = firstChildRouteData["hideIcon"]; + } + this.showReadonlyHostname = Boolean(firstChildRouteData["showReadonlyHostname"]); this.maxWidth = firstChildRouteData["maxWidth"]; - this.titleAreaMaxWidth = firstChildRouteData["titleAreaMaxWidth"]; this.hideCardWrapper = Boolean(firstChildRouteData["hideCardWrapper"]); } @@ -174,7 +177,6 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { this.pageIcon = null; this.showReadonlyHostname = null; this.maxWidth = null; - this.titleAreaMaxWidth = null; this.hideCardWrapper = null; } diff --git a/libs/components/src/anon-layout/anon-layout.component.html b/libs/components/src/anon-layout/anon-layout.component.html index a8e091e6e72..4dfde5e7ef3 100644 --- a/libs/components/src/anon-layout/anon-layout.component.html +++ b/libs/components/src/anon-layout/anon-layout.component.html @@ -13,11 +13,8 @@ -
    -
    +
    +
    @@ -36,8 +33,8 @@
    @if (hideCardWrapper) {
    diff --git a/libs/components/src/anon-layout/anon-layout.component.ts b/libs/components/src/anon-layout/anon-layout.component.ts index abde48649af..45e7f3973a9 100644 --- a/libs/components/src/anon-layout/anon-layout.component.ts +++ b/libs/components/src/anon-layout/anon-layout.component.ts @@ -10,10 +10,13 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { IconModule, Icon } from "../icon"; -import { BitwardenLogo, BitwardenShield } from "../icon/icons"; +import { BitwardenLogo } from "../icon/icons"; +import { BitwardenShield } from "../icon/logos"; import { SharedModule } from "../shared"; import { TypographyModule } from "../typography"; +export type AnonLayoutMaxWidth = "md" | "lg" | "xl" | "2xl" | "3xl"; + @Component({ selector: "auth-anon-layout", templateUrl: "./anon-layout.component.html", @@ -36,27 +39,35 @@ export class AnonLayoutComponent implements OnInit, OnChanges { @Input() hideCardWrapper: boolean = false; /** - * Max width of the title area content - * - * @default null - */ - @Input() titleAreaMaxWidth?: "md"; - - /** - * Max width of the layout content + * Max width of the anon layout title, subtitle, and content areas. * * @default 'md' */ - @Input() maxWidth: "md" | "3xl" = "md"; + @Input() maxWidth: AnonLayoutMaxWidth = "md"; protected logo = BitwardenLogo; - protected year = "2024"; + protected year: string; protected clientType: ClientType; protected hostname: string; protected version: string; protected hideYearAndVersion = false; + get maxWidthClass(): string { + switch (this.maxWidth) { + case "md": + return "tw-max-w-md"; + case "lg": + return "tw-max-w-lg"; + case "xl": + return "tw-max-w-xl"; + case "2xl": + return "tw-max-w-2xl"; + case "3xl": + return "tw-max-w-3xl"; + } + } + constructor( private environmentService: EnvironmentService, private platformUtilsService: PlatformUtilsService, @@ -68,7 +79,6 @@ export class AnonLayoutComponent implements OnInit, OnChanges { async ngOnInit() { this.maxWidth = this.maxWidth ?? "md"; - this.titleAreaMaxWidth = this.titleAreaMaxWidth ?? null; this.hostname = (await firstValueFrom(this.environmentService.environment$)).getHostname(); this.version = await this.platformUtilsService.getApplicationVersion(); diff --git a/libs/components/src/anon-layout/anon-layout.mdx b/libs/components/src/anon-layout/anon-layout.mdx index 039a1aa5f28..9d40d617b0d 100644 --- a/libs/components/src/anon-layout/anon-layout.mdx +++ b/libs/components/src/anon-layout/anon-layout.mdx @@ -165,4 +165,4 @@ import { EnvironmentSelectorComponent } from "./components/environment-selector/ --- - + diff --git a/libs/components/src/anon-layout/anon-layout.stories.ts b/libs/components/src/anon-layout/anon-layout.stories.ts index 1f4ac5bb14f..24aaf10f7ba 100644 --- a/libs/components/src/anon-layout/anon-layout.stories.ts +++ b/libs/components/src/anon-layout/anon-layout.stories.ts @@ -8,6 +8,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ButtonModule } from "../button"; +import { Icon } from "../icon"; import { LockIcon } from "../icon/icons"; import { I18nMockService } from "../utils/i18n-mock.service"; @@ -18,6 +19,23 @@ class MockPlatformUtilsService implements Partial { getClientType = () => ClientType.Web; } +type StoryArgs = Pick< + AnonLayoutComponent, + | "title" + | "subtitle" + | "showReadonlyHostname" + | "hideCardWrapper" + | "hideIcon" + | "hideLogo" + | "hideFooter" + | "maxWidth" +> & { + contentLength: "normal" | "long" | "thin"; + showSecondary: boolean; + useDefaultIcon: boolean; + icon: Icon; +}; + export default { title: "Component Library/Anon Layout", component: AnonLayoutComponent, @@ -31,12 +49,11 @@ export default { }, { provide: I18nService, - useFactory: () => { - return new I18nMockService({ + useFactory: () => + new I18nMockService({ accessing: "Accessing", appLogoLabel: "app logo label", - }); - }, + }), }, { provide: EnvironmentService, @@ -55,196 +72,179 @@ export default { ], }), ], + + render: (args: StoryArgs) => { + const { useDefaultIcon, icon, ...rest } = args; + return { + props: { + ...rest, + icon: useDefaultIcon ? null : icon, + }, + template: ` + + +
    Thin Content
    +
    +
    Long Content
    +
    Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
    +
    Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
    +
    +
    +
    Normal Content
    +
    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
    +
    +
    + +
    +
    + Secondary Projected Content (optional) +
    + +
    +
    + `, + }; + }, + + argTypes: { + title: { control: "text" }, + subtitle: { control: "text" }, + + icon: { control: false, table: { disable: true } }, + useDefaultIcon: { + control: false, + table: { disable: true }, + description: "If true, passes null so component falls back to its built-in icon", + }, + + showReadonlyHostname: { control: "boolean" }, + maxWidth: { + control: "select", + options: ["md", "lg", "xl", "2xl", "3xl"], + }, + + hideCardWrapper: { control: "boolean" }, + hideIcon: { control: "boolean" }, + hideLogo: { control: "boolean" }, + hideFooter: { control: "boolean" }, + + contentLength: { + control: "radio", + options: ["normal", "long", "thin"], + }, + + showSecondary: { control: "boolean" }, + }, + args: { title: "The Page Title", subtitle: "The subtitle (optional)", - showReadonlyHostname: true, icon: LockIcon, - hideLogo: false, + useDefaultIcon: false, + showReadonlyHostname: false, + maxWidth: "md", hideCardWrapper: false, + hideIcon: false, + hideLogo: false, + hideFooter: false, + contentLength: "normal", + showSecondary: false, }, -} as Meta; +} as Meta; -type Story = StoryObj; +type Story = StoryObj; -export const WithPrimaryContent: Story = { - render: (args) => ({ - props: args, - template: - // Projected content (the
    ) and styling is just a sample and can be replaced with any content/styling. - ` - -
    -
    Primary Projected Content Area (customizable)
    -
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
    -
    -
    - `, - }), +export const NormalPrimaryContent: Story = { + args: { + contentLength: "normal", + }, }; -export const WithSecondaryContent: Story = { - render: (args) => ({ - props: args, - template: - // Projected content (the
    's) and styling is just a sample and can be replaced with any content/styling. - // Notice that slot="secondary" is requred to project any secondary content. - ` - -
    -
    Primary Projected Content Area (customizable)
    -
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
    -
    - -
    -
    Secondary Projected Content (optional)
    - -
    -
    - `, - }), +export const LongPrimaryContent: Story = { + args: { + contentLength: "long", + }, }; -export const WithLongContent: Story = { - render: (args) => ({ - props: args, - template: - // Projected content (the
    's) and styling is just a sample and can be replaced with any content/styling. - ` - -
    -
    Primary Projected Content Area (customizable)
    -
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam? Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit.
    -
    - -
    -
    Secondary Projected Content (optional)
    -

    Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestias laborum nostrum natus. Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestias laborum nostrum natus. Expedita, quod est?

    - -
    -
    - `, - }), +export const ThinPrimaryContent: Story = { + args: { + contentLength: "thin", + }, }; -export const WithThinPrimaryContent: Story = { - render: (args) => ({ - props: args, - template: - // Projected content (the
    's) and styling is just a sample and can be replaced with any content/styling. - ` - -
    Lorem ipsum
    - -
    -
    Secondary Projected Content (optional)
    - -
    -
    - `, - }), +export const LongContentAndTitlesAndDefaultWidth: Story = { + args: { + title: + "This is a very long title that might not fit in the default width. It's really long and descriptive, so it might take up more space than usual.", + subtitle: + "This is a very long subtitle that might not fit in the default width. It's really long and descriptive, so it might take up more space than usual.", + contentLength: "long", + }, }; -export const WithCustomIcon: Story = { - render: (args) => ({ - props: args, - template: - // Projected content (the
    ) and styling is just a sample and can be replaced with any content/styling. - ` - -
    -
    Primary Projected Content Area (customizable)
    -
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
    -
    -
    - `, - }), +export const LongContentAndTitlesAndLargestWidth: Story = { + args: { + title: + "This is a very long title that might not fit in the default width. It's really long and descriptive, so it might take up more space than usual.", + subtitle: + "This is a very long subtitle that might not fit in the default width. It's really long and descriptive, so it might take up more space than usual.", + contentLength: "long", + maxWidth: "3xl", + }, }; -export const HideCardWrapper: Story = { - render: (args) => ({ - props: { - ...args, - hideCardWrapper: true, - }, - template: ` - -
    -
    Primary Projected Content Area (customizable)
    -
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
    -
    -
    -
    Secondary Projected Content (optional)
    - -
    -
    - `, - }), +export const SecondaryContent: Story = { + args: { + showSecondary: true, + }, }; -export const HideIcon: Story = { - render: (args) => ({ - props: args, - template: - // Projected content (the
    ) and styling is just a sample and can be replaced with any content/styling. - ` - -
    -
    Primary Projected Content Area (customizable)
    -
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
    -
    -
    - `, - }), +export const NoTitle: Story = { args: { title: undefined } }; + +export const NoSubtitle: Story = { args: { subtitle: undefined } }; + +export const NoWrapper: Story = { + args: { hideCardWrapper: true }, }; -export const HideLogo: Story = { - render: (args) => ({ - props: args, - template: - // Projected content (the
    ) and styling is just a sample and can be replaced with any content/styling. - ` - -
    -
    Primary Projected Content Area (customizable)
    -
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
    -
    -
    - `, - }), +export const DefaultIcon: Story = { + args: { useDefaultIcon: true }, }; -export const HideFooter: Story = { - render: (args) => ({ - props: args, - template: - // Projected content (the
    ) and styling is just a sample and can be replaced with any content/styling. - ` - -
    -
    Primary Projected Content Area (customizable)
    -
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
    -
    -
    - `, - }), +export const NoIcon: Story = { + args: { hideIcon: true }, }; -export const WithTitleAreaMaxWidth: Story = { - render: (args) => ({ - props: { - ...args, - title: "This is a very long long title to demonstrate titleAreaMaxWidth set to 'md'", - subtitle: - "This is a very long subtitle that demonstrates how the max width container handles longer text content with the titleAreaMaxWidth input set to 'md'. Lorem ipsum dolor sit amet consectetur adipisicing elit. Expedita, quod est?", - }, - template: ` - -
    -
    Primary Projected Content Area (customizable)
    -
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
    -
    -
    - `, - }), +export const NoLogo: Story = { + args: { hideLogo: true }, +}; + +export const NoFooter: Story = { + args: { hideFooter: true }, +}; + +export const ReadonlyHostname: Story = { + args: { showReadonlyHostname: true }, +}; + +export const MinimalState: Story = { + args: { + title: undefined, + subtitle: undefined, + contentLength: "normal", + hideCardWrapper: true, + hideIcon: true, + hideLogo: true, + hideFooter: true, + }, }; diff --git a/libs/components/src/button/button.component.ts b/libs/components/src/button/button.component.ts index 002b2a9d915..011360db867 100644 --- a/libs/components/src/button/button.component.ts +++ b/libs/components/src/button/button.component.ts @@ -31,9 +31,7 @@ const buttonStyles: Record = { "tw-bg-transparent", "tw-border-primary-600", "!tw-text-primary-600", - "hover:tw-bg-primary-600", - "hover:tw-border-primary-600", - "hover:!tw-text-contrast", + "hover:tw-bg-hover-default", ...focusRing, ], danger: [ diff --git a/libs/components/src/checkbox/checkbox.component.ts b/libs/components/src/checkbox/checkbox.component.ts index 079ede287cc..c420b3f3473 100644 --- a/libs/components/src/checkbox/checkbox.component.ts +++ b/libs/components/src/checkbox/checkbox.component.ts @@ -20,64 +20,75 @@ export class CheckboxComponent implements BitFormControlAbstraction { "tw-cursor-pointer", "tw-inline-block", "tw-align-sub", - "tw-rounded", - "tw-border", - "tw-border-solid", - "tw-border-secondary-500", - "tw-h-[1.12rem]", - "tw-w-[1.12rem]", - "tw-me-1.5", "tw-flex-none", // Flexbox fix for bit-form-control + "!tw-p-1", + "after:tw-inset-1", + // negative margin to negate the positioning added by the padding + "!-tw-mt-1", + "!-tw-mb-1", + "!-tw-ms-1", "before:tw-content-['']", "before:tw-block", - "before:tw-absolute", "before:tw-inset-0", + "before:tw-h-[1.12rem]", + "before:tw-w-[1.12rem]", + "before:tw-rounded", + "before:tw-border", + "before:tw-border-solid", + "before:tw-border-secondary-500", - "hover:tw-border-2", - "[&>label]:tw-border-2", + "after:tw-content-['']", + "after:tw-block", + "after:tw-absolute", + "after:tw-inset-0", + "after:tw-h-[1.12rem]", + "after:tw-w-[1.12rem]", + + "hover:before:tw-border-2", + "[&>label]:before:tw-border-2", // if it exists, the parent form control handles focus - "[&:not(bit-form-control_*)]:focus-visible:tw-ring-2", - "[&:not(bit-form-control_*)]:focus-visible:tw-ring-offset-2", - "[&:not(bit-form-control_*)]:focus-visible:tw-ring-primary-600", + "[&:not(bit-form-control_*)]:focus-visible:before:tw-ring-2", + "[&:not(bit-form-control_*)]:focus-visible:before:tw-ring-offset-2", + "[&:not(bit-form-control_*)]:focus-visible:before:tw-ring-primary-600", - "disabled:tw-cursor-auto", - "disabled:tw-border", - "disabled:hover:tw-border", - "disabled:tw-bg-secondary-100", - "disabled:hover:tw-bg-secondary-100", + "disabled:before:tw-cursor-auto", + "disabled:before:tw-border", + "disabled:before:hover:tw-border", + "disabled:before:tw-bg-secondary-100", + "disabled:hover:before:tw-bg-secondary-100", - "checked:tw-bg-primary-600", - "checked:tw-border-primary-600", - "checked:hover:tw-bg-primary-700", - "checked:hover:tw-border-primary-700", - "[&>label:hover]:checked:tw-bg-primary-700", - "[&>label:hover]:checked:tw-border-primary-700", - "checked:before:tw-bg-text-contrast", - "checked:before:tw-mask-position-[center]", - "checked:before:tw-mask-repeat-[no-repeat]", - "checked:disabled:tw-border-secondary-100", - "checked:disabled:hover:tw-border-secondary-100", - "checked:disabled:tw-bg-secondary-100", - "checked:disabled:before:tw-bg-text-muted", + "checked:before:tw-bg-primary-600", + "checked:before:tw-border-primary-600", + "checked:before:hover:tw-bg-primary-700", + "checked:before:hover:tw-border-primary-700", + "[&>label:hover]:checked:before:tw-bg-primary-700", + "[&>label:hover]:checked:before:tw-border-primary-700", + "checked:after:tw-bg-text-contrast", + "checked:after:tw-mask-position-[center]", + "checked:after:tw-mask-repeat-[no-repeat]", + "checked:disabled:before:tw-border-secondary-100", + "checked:disabled:hover:before:tw-border-secondary-100", + "checked:disabled:before:tw-bg-secondary-100", + "checked:disabled:after:tw-bg-text-muted", - "[&:not(:indeterminate)]:checked:before:tw-mask-image-[var(--mask-image)]", - "indeterminate:before:tw-mask-image-[var(--indeterminate-mask-image)]", + "[&:not(:indeterminate)]:checked:after:tw-mask-image-[var(--mask-image)]", + "indeterminate:after:tw-mask-image-[var(--indeterminate-mask-image)]", - "indeterminate:tw-bg-primary-600", - "indeterminate:tw-border-primary-600", - "indeterminate:hover:tw-bg-primary-700", - "indeterminate:hover:tw-border-primary-700", - "[&>label:hover]:indeterminate:tw-bg-primary-700", - "[&>label:hover]:indeterminate:tw-border-primary-700", - "indeterminate:before:tw-bg-text-contrast", - "indeterminate:before:tw-mask-position-[center]", - "indeterminate:before:tw-mask-repeat-[no-repeat]", - "indeterminate:before:tw-mask-image-[var(--indeterminate-mask-image)]", + "indeterminate:before:tw-bg-primary-600", + "indeterminate:before:tw-border-primary-600", + "indeterminate:hover:before:tw-bg-primary-700", + "indeterminate:hover:before:tw-border-primary-700", + "[&>label:hover]:indeterminate:before:tw-bg-primary-700", + "[&>label:hover]:indeterminate:before:tw-border-primary-700", + "indeterminate:after:tw-bg-text-contrast", + "indeterminate:after:tw-mask-position-[center]", + "indeterminate:after:tw-mask-repeat-[no-repeat]", + "indeterminate:after:tw-mask-image-[var(--indeterminate-mask-image)]", "indeterminate:disabled:tw-border-secondary-100", "indeterminate:disabled:tw-bg-secondary-100", - "indeterminate:disabled:before:tw-bg-text-muted", + "indeterminate:disabled:after:tw-bg-text-muted", ]; constructor(@Optional() @Self() private ngControl?: NgControl) {} diff --git a/libs/components/src/checkbox/checkbox.stories.ts b/libs/components/src/checkbox/checkbox.stories.ts index 123c6704ff4..9050d97cafc 100644 --- a/libs/components/src/checkbox/checkbox.stories.ts +++ b/libs/components/src/checkbox/checkbox.stories.ts @@ -13,6 +13,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { BadgeModule } from "../badge"; import { FormControlModule } from "../form-control"; +import { FormFieldModule } from "../form-field"; import { TableModule } from "../table"; import { I18nMockService } from "../utils/i18n-mock.service"; @@ -30,6 +31,7 @@ const template = /*html*/ ` @Component({ selector: "app-example", template, + imports: [CheckboxModule, FormFieldModule, ReactiveFormsModule], }) class ExampleComponent { protected formObj = this.formBuilder.group({ @@ -55,8 +57,8 @@ export default { title: "Component Library/Form/Checkbox", decorators: [ moduleMetadata({ - declarations: [ExampleComponent], imports: [ + ExampleComponent, FormsModule, ReactiveFormsModule, FormControlModule, @@ -195,17 +197,17 @@ export const Custom: Story = { props: args, template: /*html*/ `
    -
    `, diff --git a/libs/components/src/dialog/dialog.service.stories.ts b/libs/components/src/dialog/dialog.service.stories.ts index 7e2d8c62bb6..c7b8f0ae916 100644 --- a/libs/components/src/dialog/dialog.service.stories.ts +++ b/libs/components/src/dialog/dialog.service.stories.ts @@ -25,10 +25,13 @@ interface Animal { template: ` + `, - imports: [ButtonModule], + imports: [ButtonModule, LayoutComponent], }) class StoryDialogComponent { constructor(public dialogService: DialogService) {} @@ -41,6 +44,15 @@ class StoryDialogComponent { }); } + openDialogNonDismissable() { + this.dialogService.open(NonDismissableContent, { + data: { + animal: "panda", + }, + disableClose: true, + }); + } + openDrawer() { this.dialogService.openDrawer(StoryDialogContentComponent, { data: { @@ -79,13 +91,40 @@ class StoryDialogContentComponent { } } +@Component({ + template: ` + + + Dialog body text goes here. +
    + Animal: {{ animal }} +
    + + + +
    + `, + imports: [DialogModule, ButtonModule], +}) +class NonDismissableContent { + constructor( + public dialogRef: DialogRef, + @Inject(DIALOG_DATA) private data: Animal, + ) {} + + get animal() { + return this.data?.animal; + } +} + export default { title: "Component Library/Dialogs/Service", component: StoryDialogComponent, decorators: [ positionFixedWrapperDecorator(), moduleMetadata({ - declarations: [StoryDialogContentComponent], imports: [ SharedModule, ButtonModule, @@ -138,8 +177,7 @@ export const Default: Story = { }, }; -/** Drawers must be a descendant of `bit-layout`. */ -export const Drawer: Story = { +export const NonDismissable: Story = { play: async (context) => { const canvas = context.canvasElement; @@ -147,3 +185,13 @@ export const Drawer: Story = { await userEvent.click(button); }, }; + +/** Drawers must be a descendant of `bit-layout`. */ +export const Drawer: Story = { + play: async (context) => { + const canvas = context.canvasElement; + + const button = getAllByRole(canvas, "button")[2]; + await userEvent.click(button); + }, +}; diff --git a/libs/components/src/dialog/dialog/dialog.component.html b/libs/components/src/dialog/dialog/dialog.component.html index eaf7fc2beec..db08f88799b 100644 --- a/libs/components/src/dialog/dialog/dialog.component.html +++ b/libs/components/src/dialog/dialog/dialog.component.html @@ -30,15 +30,17 @@ } - + @if (!this.dialogRef?.disableClose) { + + }
    Open Simple Dialog`, + template: ` + + + + `, imports: [ButtonModule], }) class StoryDialogComponent { constructor(public dialogService: DialogService) {} - openDialog() { - this.dialogService.open(StoryDialogContentComponent, { + openSimpleDialog() { + this.dialogService.open(SimpleDialogContent, { data: { animal: "panda", }, }); } + + openNonDismissableWithPrimaryButtonDialog() { + this.dialogService.open(NonDismissableWithPrimaryButtonContent, { + data: { + animal: "panda", + }, + disableClose: true, + }); + } + + openNonDismissableWithNoButtonsDialog() { + this.dialogService.open(NonDismissableWithNoButtonsContent, { + data: { + animal: "panda", + }, + disableClose: true, + }); + } } @Component({ @@ -49,7 +76,60 @@ class StoryDialogComponent { `, imports: [ButtonModule, DialogModule], }) -class StoryDialogContentComponent { +class SimpleDialogContent { + constructor( + public dialogRef: DialogRef, + @Inject(DIALOG_DATA) private data: Animal, + ) {} + + get animal() { + return this.data?.animal; + } +} + +@Component({ + template: ` + + Dialog Title + + Dialog body text goes here. +
    + Animal: {{ animal }} +
    + + + +
    + `, + imports: [ButtonModule, DialogModule], +}) +class NonDismissableWithPrimaryButtonContent { + constructor( + public dialogRef: DialogRef, + @Inject(DIALOG_DATA) private data: Animal, + ) {} + + get animal() { + return this.data?.animal; + } +} + +@Component({ + template: ` + + Dialog Title + + Dialog body text goes here. +
    + Animal: {{ animal }} +
    +
    + `, + imports: [ButtonModule, DialogModule], +}) +class NonDismissableWithNoButtonsContent { constructor( public dialogRef: DialogRef, @Inject(DIALOG_DATA) private data: Animal, @@ -89,4 +169,29 @@ export default { type Story = StoryObj; -export const Default: Story = {}; +export const Default: Story = { + play: async (context) => { + const canvas = context.canvasElement; + + const button = getAllByRole(canvas, "button")[0]; + await userEvent.click(button); + }, +}; + +export const NonDismissableWithPrimaryButton: Story = { + play: async (context) => { + const canvas = context.canvasElement; + + const button = getAllByRole(canvas, "button")[1]; + await userEvent.click(button); + }, +}; + +export const NonDismissableWithNoButtons: Story = { + play: async (context) => { + const canvas = context.canvasElement; + + const button = getAllByRole(canvas, "button")[2]; + await userEvent.click(button); + }, +}; diff --git a/libs/components/src/form-control/form-control.component.html b/libs/components/src/form-control/form-control.component.html index 735e375a29a..15d422b01a1 100644 --- a/libs/components/src/form-control/form-control.component.html +++ b/libs/components/src/form-control/form-control.component.html @@ -1,5 +1,5 @@