diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d7cca18960a..91cd174a6b6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -162,3 +162,7 @@ apps/web/src/locales/en/messages.json **/*.Dockerfile **/.dockerignore **/entrypoint.sh + +## Overrides +# tsconfig files are potentially dangerous and will be reviewed by platform to prevent misconfigurations +**/tsconfig.json @bitwarden/team-platform-dev diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index ff85a30d3f6..d758e6f11c9 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -222,7 +222,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'success' - deployment_id: ${{ needs.setup.outputs.deployment_id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} - name: Update deployment status to Failure if: ${{ inputs.publish_type != 'Dry Run' && failure() }} @@ -230,4 +230,4 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'failure' - deployment_id: ${{ needs.setup.outputs.deployment_id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml index af24083e973..ae631165db9 100644 --- a/.github/workflows/publish-desktop.yml +++ b/.github/workflows/publish-desktop.yml @@ -162,7 +162,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'success' - deployment_id: ${{ needs.setup.outputs.deployment_id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} - name: Update deployment status to Failure if: ${{ inputs.publish_type != 'Dry Run' && failure() }} @@ -170,7 +170,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'failure' - deployment_id: ${{ needs.setup.outputs.deployment_id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} snap: name: Deploy Snap @@ -283,7 +283,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'success' - deployment_id: ${{ needs.setup.outputs.deployment_id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} - name: Update deployment status to Failure if: ${{ inputs.publish_type != 'Dry Run' && failure() }} @@ -291,4 +291,4 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'failure' - deployment_id: ${{ needs.setup.outputs.deployment_id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} diff --git a/.github/workflows/release-browser.yml b/.github/workflows/release-browser.yml index 3e54e79a303..498f8748959 100644 --- a/.github/workflows/release-browser.yml +++ b/.github/workflows/release-browser.yml @@ -40,7 +40,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release_version-check@main + uses: bitwarden/gh-actions/release-version-check@main with: release-type: ${{ github.event.inputs.release_type }} project-type: ts diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index a1bc36cf85e..6a10bec1ba2 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -40,7 +40,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release_version-check@main + uses: bitwarden/gh-actions/release-version-check@main with: release-type: ${{ inputs.release_type }} project-type: ts diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index ea0feb10e3d..a5e374395d8 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -47,7 +47,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release_version-check@main + uses: bitwarden/gh-actions/release-version-check@main with: release-type: 'Initial Release' project-type: ts diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index 3285ad468d6..57143747a86 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -40,7 +40,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release_version-check@main + uses: bitwarden/gh-actions/release-version-check@main with: release-type: ${{ inputs.release_type }} project-type: ts diff --git a/apps/browser/package.json b/apps/browser/package.json index 1b31af0d150..8fc1d733921 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,24 +1,24 @@ { "name": "@bitwarden/browser", - "version": "2025.1.1", + "version": "2025.1.4", "scripts": { "build": "npm run build:chrome", - "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 webpack", - "build:edge": "cross-env BROWSER=edge MANIFEST_VERSION=3 webpack", - "build:firefox": "cross-env BROWSER=firefox webpack", - "build:opera": "cross-env BROWSER=opera webpack", - "build:safari": "cross-env BROWSER=safari webpack", + "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", + "build:edge": "cross-env BROWSER=edge MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", + "build:firefox": "cross-env BROWSER=firefox NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", + "build:opera": "cross-env BROWSER=opera NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", + "build:safari": "cross-env BROWSER=safari NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", "build:watch": "npm run build:watch:chrome", "build:watch:chrome": "npm run build:chrome -- --watch", "build:watch:edge": "npm run build:edge -- --watch", "build:watch:firefox": "npm run build:firefox -- --watch", "build:watch:opera": "npm run build:opera -- --watch", "build:watch:safari": "npm run build:safari -- --watch", - "build:prod:chrome": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:chrome", - "build:prod:edge": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:edge", - "build:prod:firefox": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:firefox", - "build:prod:opera": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:opera", - "build:prod:safari": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:safari", + "build:prod:chrome": "cross-env NODE_ENV=production npm run build:chrome", + "build:prod:edge": "cross-env NODE_ENV=production npm run build:edge", + "build:prod:firefox": "cross-env NODE_ENV=production npm run build:firefox", + "build:prod:opera": "cross-env NODE_ENV=production npm run build:opera", + "build:prod:safari": "cross-env NODE_ENV=production npm run build:safari", "dist:chrome": "npm run build:prod:chrome && mkdir -p dist && ./scripts/compress.ps1 dist-chrome.zip", "dist:edge": "npm run build:prod:edge && mkdir -p dist && ./scripts/compress.ps1 dist-edge.zip", "dist:firefox": "npm run build:prod:firefox && mkdir -p dist && ./scripts/compress.ps1 dist-firefox.zip", diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 625454de8c6..807565f3749 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "قم بتأكيد هويتك" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "خزانتك مقفلة. قم بتأكيد هويتك للمتابعة." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "اطلب إضافة عنصر إذا لم يتم العثور على عنصر في المخزن الخاص بك. ينطبق على جميع حسابات تسجيل الدخول." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "أظهر البطاقات في صفحة التبويبات" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "قائمة عناصر البطاقة في صفحة التبويب لسهولة التعبئة التلقائية." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "إظهار الهويات على صفحة التبويب" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 8e7d2201f28..153295bcd01 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Kimliyi doğrula" }, + "weDontRecognizeThisDevice": { + "message": "Bu cihazı tanımırıq. Kimliyinizi doğrulamaq üçün e-poçtunuza göndərilən kodu daxil edin." + }, + "continueLoggingIn": { + "message": "Giriş etməyə davam" + }, "yourVaultIsLocked": { "message": "Seyfiniz kilidlənib. Davam etmək üçün kimliyinizi doğrulayın." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Seyfinizdə tapılmayan elementin əlavə edilməsi soruşulsun. Giriş etmiş bütün hesablara aiddir." }, - "showCardsInVaultView": { - "message": "Kartları, Seyf görünüşündə Avto-doldurma təklifləri olaraq göstər" + "showCardsInVaultViewV2": { + "message": "Kartları, Seyf görünüşündə Avto-doldurma təklifləri olaraq həmişə göstər" }, "showCardsCurrentTab": { "message": "Kartları Vərəq səhifəsində göstər" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Asan avto-doldurma üçün Vərəq səhifəsində kart elementlərini sadalayın." }, - "showIdentitiesInVaultView": { - "message": "Kimlikləri, Seyf görünüşündə Avto-doldurma təklifləri olaraq göstər" + "showIdentitiesInVaultViewV2": { + "message": "Kimlikləri, Seyf görünüşündə Avto-doldurma təklifləri olaraq həmişə göstər" }, "showIdentitiesCurrentTab": { "message": "Vərəq səhifəsində kimlikləri göstər" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Ekstra enli" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Lütfən masaüstü tətbiqinizi güncəlləyin" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Biometrik kilid açmanı istifadə etmək üçün lütfən masaüstü tətbiqinizi güncəlləyin, ya da masaüstü ayarlarında barmaq izi ilə kilid açmanı sıradan çıxardın." } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 57496e83f41..5d898ff5674 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Праверыць асобу" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Ваша сховішча заблакіравана. Каб працягнуць, пацвердзіце сваю асобу." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Паказваць карткі на старонцы з укладкамі" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Спіс элементаў картак на старонцы з укладкамі для лёгкага аўтазапаўнення." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Паказваць пасведчанні на старонцы з укладкамі" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 6e9f992f5ba..da5ba937925 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Потвърждаване на самоличността" }, + "weDontRecognizeThisDevice": { + "message": "Това устройство е непознато. Въведете кода изпратен на е-пощата Ви, за да потвърдите самоличността си." + }, + "continueLoggingIn": { + "message": "Продължаване с вписването" + }, "yourVaultIsLocked": { "message": "Трезорът е заключен — въведете главната си парола, за да продължите." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Питане за добавяне на елемент, ако такъв не бъде намерен в трезора. Прилага се за всички регистрации, в които сте вписан(а)." }, - "showCardsInVaultView": { - "message": "Показване на картите като предложения за авт. попълване в изгледа на трезора" + "showCardsInVaultViewV2": { + "message": "Картите да се показват винаги като предложения за авт. попълване в изгледа на трезора" }, "showCardsCurrentTab": { "message": "Показване на карти в страницата с разделите" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Показване на картите в страницата с разделите, за лесно автоматично попълване." }, - "showIdentitiesInVaultView": { - "message": "Показване на самоличности като предложения за автоматично попълване в изгледа на трезора" + "showIdentitiesInVaultViewV2": { + "message": "Самоличностите да се показват винаги като предложения за автоматично попълване в изгледа на трезора" }, "showIdentitiesCurrentTab": { "message": "Показване на самоличности в страницата с разделите" @@ -2285,7 +2291,7 @@ "message": "Потребителят е заключен или отписан" }, "biometricsNotUnlockedDesc": { - "message": "Отключете потребителя в настолното приложение и опитайте отново." + "message": "Отключете потребителя в самостоятелното приложение и опитайте отново." }, "biometricsNotAvailableTitle": { "message": "Отключването чрез биометрични данни не е налично" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Много широко" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Моля, обновете самостоятелното приложение" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "За да използвате отключването чрез биометрични данни, обновете самостоятелното приложение или изключете отключването чрез пръстов отпечатък в настройките му." } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 219071238e9..2e992713c14 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "আপনার ভল্ট লক করা আছে। চালিয়ে যেতে আপনার মূল পাসওয়ার্ডটি যাচাই করান।" }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 81c162e91be..be803b0a077 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 89ab22c1794..b2252e31e5b 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Verifica identitat" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "La caixa forta està bloquejada. Comproveu la contrasenya mestra per continuar." }, @@ -978,7 +984,7 @@ "message": "Demana d'afegir els inicis de sessió" }, "vaultSaveOptionsTitle": { - "message": "Save to vault options" + "message": "Opcions de guardar a la caixa forta" }, "addLoginNotificationDesc": { "message": "La \"Notificació per afegir inicis de sessió\" demana automàticament que guardeu els nous inicis de sessió a la vostra caixa forta quan inicieu la sessió per primera vegada." @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Demana afegir un element si no se'n troba cap a la caixa forta. S'aplica a tots els comptes connectats." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Mostra sempre les targetes com a suggeriments d'emplenament automàtic a la vista de la caixa forta" }, "showCardsCurrentTab": { "message": "Mostra les targetes a la pàgina de pestanya" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Llista els elements de la targeta a la pàgina de pestanya per facilitar l'autoemplenat." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Mostra sempre les identitats com a suggeriments d'emplenament automàtic a la vista de la caixa forta" }, "showIdentitiesCurrentTab": { "message": "Mostra les identitats a la pàgina de pestanya" @@ -1005,7 +1011,7 @@ "message": "Llista els elements d'identitat de la pestanya de la pàgina per facilitar l'autoemplenat." }, "clickToAutofillOnVault": { - "message": "Click items to autofill on Vault view" + "message": "Feu clic als elements per emplenar automàticament a la vista de la caixa forta" }, "clearClipboard": { "message": "Buida el porta-retalls", @@ -1122,11 +1128,11 @@ "message": "\"Contrasenya del fitxer\" i \"Confirma contrasenya del fitxer\" no coincideixen." }, "warning": { - "message": "ADVERTIMENT", + "message": "AVÍS", "description": "WARNING (should stay in capitalized letters if the language permits)" }, "warningCapitalized": { - "message": "Warning", + "message": "Advertència", "description": "Warning (should maintain locale-relevant capitalization)" }, "confirmVaultExport": { @@ -1466,10 +1472,10 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Autofill suggestions" + "message": "Suggeriments d'emplenament automàtic" }, "showInlineMenuLabel": { - "message": "Show autofill suggestions on form fields" + "message": "Mostra suggeriments d'emplenament automàtic als camps del formulari" }, "showInlineMenuIdentitiesLabel": { "message": "Display identities as suggestions" @@ -1478,7 +1484,7 @@ "message": "Display cards as suggestions" }, "showInlineMenuOnIconSelectionLabel": { - "message": "Display suggestions when icon is selected" + "message": "Mostra suggeriments quan la icona està seleccionada" }, "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." @@ -1502,7 +1508,7 @@ "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoadSectionTitle": { - "message": "Autofill on page load" + "message": "Emplenament automàtic a la càrrega de la pàgina" }, "enableAutoFillOnPageLoad": { "message": "Habilita l'emplenament automàtic en carregar la pàgina" @@ -1514,7 +1520,7 @@ "message": "Els llocs web compromesos o no fiables poden aprofitar-se de l'emplenament automàtic en carregar de la pàgina." }, "learnMoreAboutAutofillOnPageLoadLinkText": { - "message": "Learn more about risks" + "message": "Més informació sobre riscs" }, "learnMoreAboutAutofill": { "message": "Obteniu més informació sobre l'emplenament automàtic" @@ -1768,7 +1774,7 @@ "message": "Identitat" }, "typeSshKey": { - "message": "SSH key" + "message": "Clau SSH" }, "newItemHeader": { "message": "New $TYPE$", @@ -1801,13 +1807,13 @@ "message": "Historial de les contrasenyes" }, "generatorHistory": { - "message": "Generator history" + "message": "Historial del generador" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "Neteja l'historial del generador" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "Si continueu, totes les entrades se suprimiran permanentment de l'historial del generador. Esteu segur que voleu continuar?" }, "back": { "message": "Arrere" @@ -1846,7 +1852,7 @@ "message": "Notes segures" }, "sshKeys": { - "message": "SSH Keys" + "message": "Claus SSH" }, "clear": { "message": "Esborra", @@ -1872,7 +1878,7 @@ "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "Domini base (recomanat)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -2041,10 +2047,10 @@ "message": "Clona" }, "passwordGenerator": { - "message": "Password generator" + "message": "Generador de contrasenyes" }, "usernameGenerator": { - "message": "Username generator" + "message": "Generador de nom d'usuari" }, "useThisPassword": { "message": "Use this password" @@ -2108,7 +2114,7 @@ "message": "Ompli automàticament i guarda" }, "fillAndSave": { - "message": "Fill and save" + "message": "Ompli i guarda" }, "autoFillSuccessAndSavedUri": { "message": "Element emplenat automàticament i URI guardat" @@ -2825,7 +2831,7 @@ "message": "Generate email" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "El valor ha d'estar entre $MIN$ i $MAX$.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -2839,7 +2845,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": " Utilitzeu $RECOMMENDED$ caràcters o més per generar una contrasenya segura.", "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": { @@ -2849,7 +2855,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Utilitzeu paraules $RECOMMENDED$ o més per generar una frase de pas segura.", + "message": " Utilitzeu $RECOMMENDED$ paraules o més per generar una frase de pas segura.", "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": { @@ -3175,13 +3181,13 @@ "message": "Configuració d'emplenament automàtic" }, "autofillKeyboardShortcutSectionTitle": { - "message": "Autofill shortcut" + "message": "Drecera d'emplenament automàtic" }, "autofillKeyboardShortcutUpdateLabel": { - "message": "Change shortcut" + "message": "Canvia la drecera" }, "autofillKeyboardManagerShortcutsLabel": { - "message": "Manage shortcuts" + "message": "Gestiona les dreceres" }, "autofillShortcut": { "message": "Drecera de teclat d'emplenament automàtic" @@ -3190,7 +3196,7 @@ "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, "autofillLoginShortcutText": { - "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", + "message": "La drecera d'emplenament automàtic és $COMMAND$. Gestioneu totes les dreceres a la configuració del navegador.", "placeholders": { "command": { "content": "$1", @@ -3302,11 +3308,11 @@ "message": "Dispositiu de confiança" }, "sendsNoItemsTitle": { - "message": "No active Sends", + "message": "No hi ha Sends actius", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsNoItemsMessage": { - "message": "Use Send to securely share encrypted information with anyone.", + "message": "Utilitzeu Send per compartir informació xifrada de manera segura amb qualsevol persona.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { @@ -3976,7 +3982,7 @@ "message": "Clau de pas suprimida" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "Suggeriments d'emplenament automàtic" }, "itemSuggestions": { "message": "Suggested items" @@ -3994,7 +4000,7 @@ "message": "Clear filters or try another search term" }, "copyInfoTitle": { - "message": "Copy info - $ITEMNAME$", + "message": "Copia info - $ITEMNAME$", "description": "Title for a button that opens a menu with options to copy information from an item.", "placeholders": { "itemname": { @@ -4014,7 +4020,7 @@ } }, "moreOptionsLabel": { - "message": "More options, $ITEMNAME$", + "message": "Més opcions, $ITEMNAME$", "description": "Aria label for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4024,7 +4030,7 @@ } }, "moreOptionsTitle": { - "message": "More options - $ITEMNAME$", + "message": "Més opcions - $ITEMNAME$", "description": "Title for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4072,13 +4078,13 @@ "message": "Consola d'administració" }, "accountSecurity": { - "message": "Account security" + "message": "Seguretat del compte" }, "notifications": { - "message": "Notifications" + "message": "Notificacions" }, "appearance": { - "message": "Appearance" + "message": "Aparença" }, "errorAssigningTargetCollection": { "message": "S'ha produït un error en assignar la col·lecció de destinació." @@ -4208,7 +4214,7 @@ "message": "Filtres" }, "filterVault": { - "message": "Filter vault" + "message": "Filtra dades" }, "filterApplied": { "message": "One filter applied" @@ -4304,7 +4310,7 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "Habilita l'emplenament automàtic en carregar la pàgina?" }, "cardExpiredTitle": { "message": "Expired card" @@ -4328,7 +4334,7 @@ "message": "Enable animations" }, "showAnimations": { - "message": "Show animations" + "message": "Mostra animacions" }, "addAccount": { "message": "Afig compte" @@ -4561,10 +4567,10 @@ "message": "Account actions" }, "showNumberOfAutofillSuggestions": { - "message": "Show number of login autofill suggestions on extension icon" + "message": "Mostra el nombre de suggeriments d'emplenament automàtic d'inici de sessió a la icona d'extensió" }, "showQuickCopyActions": { - "message": "Show quick copy actions on Vault" + "message": "Mostra accions de còpia ràpida a la caixa forta" }, "systemDefault": { "message": "System default" @@ -4573,16 +4579,16 @@ "message": "Enterprise policy requirements have been applied to this setting" }, "sshPrivateKey": { - "message": "Private key" + "message": "Clau privada" }, "sshPublicKey": { - "message": "Public key" + "message": "Clau pública" }, "sshFingerprint": { "message": "Fingerprint" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "Tipus de clau" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -4819,7 +4825,7 @@ "message": "Generated password" }, "compactMode": { - "message": "Compact mode" + "message": "Mode compacte" }, "beta": { "message": "Beta" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 0a05974279a..eaad6b1e643 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Ověřit identitu" }, + "weDontRecognizeThisDevice": { + "message": "Toto zařízení nepoznáváme. Zadejte kód zaslaný na Váš e-mail pro ověření Vaší totožnosti." + }, + "continueLoggingIn": { + "message": "Pokračovat v přihlášení" + }, "yourVaultIsLocked": { "message": "Váš trezor je uzamčen. Pro pokračování musíte zadat hlavní heslo." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Požádá o přidání položky, pokud nebyla nalezena v trezoru. Platí pro všechny přihlášené účty." }, - "showCardsInVaultView": { - "message": "Zobrazit karty jako návrhy automatického vyplňování v zobrazení trezoru" + "showCardsInVaultViewV2": { + "message": "Vždy zobrazit karty jako návrhy automatického vyplňování v zobrazení trezoru" }, "showCardsCurrentTab": { "message": "Zobrazit platební karty na obrazovce Karta" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Pro snadné vyplnění zobrazí platební karty na obrazovce Karta." }, - "showIdentitiesInVaultView": { - "message": "Zobrazit identity jako návrhy automatického vyplňování v zobrazení trezoru" + "showIdentitiesInVaultViewV2": { + "message": "Vždy zobrazit identity jako návrhy automatického vyplňování v zobrazení trezoru" }, "showIdentitiesCurrentTab": { "message": "Zobrazit identity na obrazovce Karta" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra široký" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Aktualizujte aplikaci pro stolní počítač" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Chcete-li použít biometrické odemknutí, aktualizujte aplikaci pro stolní počítač nebo v nastavení vypněte odemknutí otiskem prstů." } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index cbaa31fea30..174f91a9914 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -29,7 +29,7 @@ "message": "Use single sign-on" }, "welcomeBack": { - "message": "Welcome back" + "message": "Croeso nôl" }, "setAStrongPassword": { "message": "Gosod cyfrinair cryf" @@ -171,7 +171,7 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Copïo'r wefan" }, "copyNotes": { "message": "Copy notes" @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Gwirio'ch hunaniaeth" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Mae eich cell ar glo. Gwiriwch eich hunaniaeth i barhau." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -4075,7 +4081,7 @@ "message": "Account security" }, "notifications": { - "message": "Notifications" + "message": "Hysbysiadau" }, "appearance": { "message": "Golwg" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 628ce983c09..a35cdcb7435 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Bekræft identitet" }, + "weDontRecognizeThisDevice": { + "message": "Denne enhed er ikke genkendt. Angiv koden i den tilsendte e-mail for at bekræfte identiteten." + }, + "continueLoggingIn": { + "message": "Fortsæt med at logge ind" + }, "yourVaultIsLocked": { "message": "Din boks er låst. Bekræft din identitet for at fortsætte." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Anmod om at tilføje et emne, hvis intet ikke findes i boksen. Gælder alle indloggede konti." }, - "showCardsInVaultView": { - "message": "Vis kort som Autoudfyldningsforslag ved Boks-visning" + "showCardsInVaultViewV2": { + "message": "Vis altid kort som Autoudfyldningsforslag ved Boks-visning" }, "showCardsCurrentTab": { "message": "Vis kort på fanebladet" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Vis kortemner på siden Fane for nem autoudfyldning." }, - "showIdentitiesInVaultView": { - "message": "Vis identiteter som Autoudfyldningsforslag ved Boks-visning" + "showIdentitiesInVaultViewV2": { + "message": "Vis altid identiteter som Autoudfyldningsforslag ved Boks-visning" }, "showIdentitiesCurrentTab": { "message": "Vis identiteter på fanebladet" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Ekstra bred" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Opdatér venligst computerapplikationen" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "For brug af biometrisk oplåsning skal computerapplikationen opdateres eller fingeraftryksoplåsning deaktiveres i computerindstillingerne." } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 86d9d513e40..75f616e3152 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Identität verifizieren" }, + "weDontRecognizeThisDevice": { + "message": "Wir erkennen dieses Gerät nicht. Gib den an deine E-Mail-Adresse gesendeten Code ein, um deine Identität zu verifizieren." + }, + "continueLoggingIn": { + "message": "Anmeldung fortsetzen" + }, "yourVaultIsLocked": { "message": "Dein Tresor ist gesperrt. Verifiziere deine Identität, um fortzufahren." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Nach dem Hinzufügen eines Eintrags fragen, wenn er nicht in deinem Tresor gefunden wurde. Gilt für alle angemeldeten Konten." }, - "showCardsInVaultView": { - "message": "Karten als Vorschläge zum Auto-Ausfüllen in der Tresor-Ansicht anzeigen" + "showCardsInVaultViewV2": { + "message": "Karten immer als Auto-Ausfüllen-Vorschläge in der Tresor-Ansicht anzeigen" }, "showCardsCurrentTab": { "message": "Karten auf Tab Seite anzeigen" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Karten-Einträge auf der Tab Seite anzeigen, um das Auto-Ausfüllen zu vereinfachen." }, - "showIdentitiesInVaultView": { - "message": "Identitäten als Vorschläge zum Auto-Ausfüllen in der Tresor-Ansicht anzeigen" + "showIdentitiesInVaultViewV2": { + "message": "Identitäten immer als Auto-Ausfüllen-Vorschläge in der Tresor-Ansicht anzeigen" }, "showIdentitiesCurrentTab": { "message": "Identitäten auf Tab Seite anzeigen" @@ -2811,7 +2817,7 @@ "message": "Bitwarden konnte folgende(n) Tresor-Eintrag/Einträge nicht entschlüsseln." }, "contactCSToAvoidDataLossPart1": { - "message": "Kontaktiere den Kundensupport", + "message": "Kontaktiere unser Customer Success Team", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra breit" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Bitte aktualisiere deine Desktop-Anwendung" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Um biometrisches Entsperren zu verwenden, aktualisiere bitte deine Desktop-Anwendung oder deaktiviere die Entsperrung per Fingerabdruck in den Desktop-Einstellungen." } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 16fd5094fdd..24dfa75a3b6 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Επιβεβαίωση ταυτότητας" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Το vault σας είναι κλειδωμένο. Επαληθεύστε τον κύριο κωδικό πρόσβασης για να συνεχίσετε." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ζητήστε να προσθέσετε ένα αντικείμενο αν δε βρεθεί στο θησαυ/κιό σας. Ισχύει για όλους τους συνδεδεμένους λογαριασμούς." }, - "showCardsInVaultView": { - "message": "Εμφάνιση καρτών ως προτάσεις αυτόματης συμπλήρωσης στην προβολή Θησαυ/κίου" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Εμφάνιση καρτών στη σελίδα Καρτέλας" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Εμφάνισε τα αντικείμενα κάρτες στη σελίδα Καρτέλα για εύκολη αυτόματη συμπλήρωση." }, - "showIdentitiesInVaultView": { - "message": "Εμφάνιση ταυτοτήτων ως προτάσεις αυτόματης συμπλήρωσης στην προβολή Θησαυ/κίου" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Εμφάνιση ταυτοτήτων στη σελίδα καρτέλας" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Εξαιρετικά φαρδύ" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 6b1764289f8..666dea3f5b8 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 4ea532f1e35..70ebff74e72 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognise this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy auto-fill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 38724849736..2a4d0c67fca 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Verify Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognise this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy auto-fill." }, - "showIdentitiesInVaultView": { - "message": "Show identifies as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 2f87902cedb..44a7d564a06 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Verificar identidad" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Tu caja fuerte está bloqueada. Verifica tu identidad para continuar." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Pide que se agregue un elemento si no se encuentra uno en su caja fuerte. Se aplica a todas las cuentas que hayan iniciado sesión." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Mostrar las tarjetas en la pestaña" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Listar los elementos de tarjetas en la página para facilitar el auto-rellenado." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Mostrar las identidades en la página" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 5ee8566dc33..9f108a5666c 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Identiteedi kinnitamine" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Hoidla on lukus. Jätkamiseks sisesta ülemparool." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Kuva \"Kaart\" vaates kaardiandmed" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Kuvab \"Kaart\" vaates kaardiandmeid, et neid saaks kiiresti sisestada" }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Kuva \"Kaart\" vaates identiteete" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index d7bcfc2838e..2494046aaab 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Zure identitatea egiaztatu" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Zure kutxa gotorra blokeatuta dago. Egiaztatu zure identitatea jarraitzeko." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Erakutsi txartelak fitxa orrian" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Erakutsi elementuen txartelak fitxa orrian, erraz auto-betetzeko." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Erakutsi identitateak fitxa orrian" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 3cb1581b98f..f9f016ecac2 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "تأیید هویت" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "گاوصندوق شما قفل شده است. برای ادامه هویت خود را تأیید کنید." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "نمایش کارت‌ها در صفحه برگه" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "برای پر کردن خودکار آسان، موارد کارت را در صفحه برگه فهرست کن." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "نشان دادن هویت در صفحه برگه" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 9e2f18ea72e..a225f89ced5 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Vahvista henkilöllisyytesi" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Holvisi on lukittu. Jatka vahvistamalla henkilöllisyytesi." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ehdota kohteen tallennusta, jos holvistasi ei vielä löydy vastaavaa kohdetta. Koskee kaikkia kirjautuneita tilejä." }, - "showCardsInVaultView": { - "message": "Näytä kortit automaattitäytön ehdotuksina Holvi-näkymässä" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Näytä kortit välilehtiosiossa" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Näytä kortit Välilehti-sivulla automaattitäytön helpottamiseksi." }, - "showIdentitiesInVaultView": { - "message": "Näytä henkilöllisyydet automaattitäytön ehdotuksina Holvi-sivulla." + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Näytä henkilöllisyydet välilehtiosiossa" @@ -2325,7 +2331,7 @@ "description": "A category title describing the concept of web domains" }, "blockedDomains": { - "message": "Blocked domains" + "message": "Estetyt verkkotunnukset" }, "excludedDomains": { "message": "Ohitettavat verkkotunnukset" @@ -2340,7 +2346,7 @@ "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, "autofillBlockedNoticeV2": { - "message": "Autofill is blocked for this website." + "message": "Automaattitäyttö on estetty tällä sivustolla." }, "autofillBlockedNoticeGuidance": { "message": "Change this in settings" @@ -2805,7 +2811,7 @@ "message": "Virhe" }, "decryptionError": { - "message": "Decryption error" + "message": "Salauksen purkuvirhe" }, "couldNotDecryptVaultItemsBelow": { "message": "Bitwarden could not decrypt the vault item(s) listed below." @@ -3976,10 +3982,10 @@ "message": "Pääsyavain poistettiin" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "Automaattitäytön ehdotukset" }, "itemSuggestions": { - "message": "Suggested items" + "message": "Ehdotetut kohteet" }, "autofillSuggestionsTip": { "message": "Tallenna tälle sivustolle automaattisesti täytettävä kirjautumistieto." @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Erittäin leveä" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 62e962e309a..91c94013c5c 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "I-verify ang pagkakakilanlan" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Naka-lock ang iyong vault. Patunayan ang iyong pagkakakilanlan upang magpatuloy." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Hilingin na magdagdag ng isang item kung ang isa ay hindi mahanap sa iyong vault. Nalalapat sa lahat ng naka-log in na account." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Ipakita ang mga card sa Tab page" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Itala ang mga item ng card sa Tab page para sa madaling auto-fill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Ipakita ang mga pagkatao sa Tab page" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index b1ebfe05fe1..d155db81547 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Vérifier l'identité" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Votre coffre est verrouillé. Vérifiez votre identité pour continuer." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Demande l'ajout d'un élément si celui-ci n'est pas trouvé dans votre coffre. S'applique à tous les comptes connectés." }, - "showCardsInVaultView": { - "message": "Afficher les cartes de paiement en tant que suggestions de saisie automatique dans la vue du coffre" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Afficher les cartes de paiement sur la Page d'onglet" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Liste les éléments des cartes de paiement sur la Page d'onglet pour faciliter la saisie automatique." }, - "showIdentitiesInVaultView": { - "message": "Afficher les identités en tant que suggestions de saisie automatique dans la vue du coffre" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Afficher les identités sur la Page d'onglet" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Très large" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 3d352359e69..d5d956f7f0d 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Verificar identidade" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "A túa caixa forte está bloqueada. Verifica a túa identidade para continuar." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ofrecer gardar un elemento se non se atopa na caixa forte. Aplica a tódalas sesións iniciadas." }, - "showCardsInVaultView": { - "message": "Na caixa forte, amosar tarxetas como suxestións de Autoenchido" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Amosar tarxetas na pestana" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Lista na pestana actual as tarxetas gardadas para autoenchido." }, - "showIdentitiesInVaultView": { - "message": "Na caixa forte, amosar identidades como suxestións de Autoenchido" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Amosar identidades na pestana" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Moi ancho" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index b66d0a4aa24..ef2b54903df 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "אימות זהות" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "הכספת שלך נעולה. הזן את הסיסמה הראשית שלך כדי להמשיך." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 8927c78e16b..1c3869fa4e5 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "पहचान सत्यापित करें" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "आपकी वॉल्ट लॉक हो गई है। जारी रखने के लिए अपने मास्टर पासवर्ड को सत्यापित करें।" }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "टैब पेज पर कार्ड दिखाएं" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "आसान ऑटो-फिल के लिए टैब पेज पर कार्ड आइटम सूचीबद्ध करें।" }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "टैब पेज पर पहचान दिखाएं" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 20583c4cbbd..11831d13ea5 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Potvrdi identitet" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Tvoj trezor je zaključan. Potvrdi glavnu lozinku za nastavak." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Pitaj za dodavanje stavke ako nije pronađena u tvojem trezoru. Primjenjuje se na sve prijavljene račune." }, - "showCardsInVaultView": { - "message": "Prikaži kartice kao prijedloge za auto-ispunu u prikazu trezora" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Prikaži platne kartice" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Prikazuj platne kartice za jednostavnu auto-ispunu." }, - "showIdentitiesInVaultView": { - "message": "Prikaži identitete kao prijedloge za auto-ispunu u prikazu trezora" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Prikaži identitete" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Ekstra široko" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index bcde4db3c4f..d1e9abdaaed 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Személyazonosság ellenőrzése" }, + "weDontRecognizeThisDevice": { + "message": "Nem ismerhető fel ez az eszköz. Írjuk be az email címünkre küldött kódot a személyazonosság igazolásához." + }, + "continueLoggingIn": { + "message": "A bejelentkezés folytatása" + }, "yourVaultIsLocked": { "message": "A széf zárolásra került. A folytatáshoz meg kell adni a mesterjelszót." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Egy elem hozzáadásának kérése, ha az nem található a széfben. Minden bejelentkezett fiókra vonatkozik." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Mindig jelenítse meg a kártyákat automatikus kitöltési javaslatként a Széf nézetben" }, "showCardsCurrentTab": { "message": "Kártyák megjelenítése a Fül oldalon" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Kártyaelemek listázása a Fül oldalon a könnyű automatikus kitöltéshez." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Mindig jelenítse meg a személyazonosságokat automatikus kitöltési javaslatként a Széf nézetben" }, "showIdentitiesCurrentTab": { "message": "Azonosítások megjelenítése a Fül oldalon" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra széles" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Frissítsük az asztali alkalmazást." + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "A biometrikus feloldás használatához frissítsük az asztali alkalmazást vagy tiltsuk le az ujjlenyomatos feloldást az asztali beállításokban." } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index fedfb0cec1f..0964684ecff 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Verifikasi Identitas Anda" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Brankas Anda terkunci. Verifikasi kata sandi utama Anda untuk melanjutkan." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Tanyakan untuk menambah sebuah benda jika benda itu tidak ditemukan di brankas Anda. Diterapkan ke seluruh akun yang telah masuk." }, - "showCardsInVaultView": { - "message": "Tampilkan kartu sebagai saran isi otomatis pada tampilan Brankas" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Tamplikan kartu pada halaman Tab" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Buat tampilan daftar benda dari kartu pada halaman Tab untuk isi otomatis yang mudah." }, - "showIdentitiesInVaultView": { - "message": "Tampilkan identitas sebagai saran isi otomatis pada tampilan Brankas" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Tampilkan identitas pada halaman Tab" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Ekstra lebar" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 53b94a951c1..c36addb8df1 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Verifica identità" }, + "weDontRecognizeThisDevice": { + "message": "Non riconosciamo questo dispositivo. Inserisci il codice inviato alla tua e-mail per verificare la tua identità." + }, + "continueLoggingIn": { + "message": "Continua l'accesso" + }, "yourVaultIsLocked": { "message": "La tua cassaforte è bloccata. Verifica la tua identità per continuare." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Chiedi di creare un nuovo elemento se non ce n'è uno nella tua cassaforte. Si applica a tutti gli account sul dispositivo." }, - "showCardsInVaultView": { - "message": "Mostra le carte come suggerimenti di riempimento automatico nella vista cassaforte" + "showCardsInVaultViewV2": { + "message": "Mostra sempre le carte come suggerimenti di riempimento automatico nella vista cassaforte" }, "showCardsCurrentTab": { "message": "Mostra le carte nella sezione Scheda" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Mostra le carte nella sezione Scheda per riempirle automaticamente." }, - "showIdentitiesInVaultView": { - "message": "Mostra le identità come suggerimenti di riempimento automatico nella vista cassaforte" + "showIdentitiesInVaultViewV2": { + "message": "Mostra sempre le identità come suggerimenti di riempimento automatico nella vista cassaforte" }, "showIdentitiesCurrentTab": { "message": "Mostra le identità nella sezione Scheda" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Molto larga" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Aggiornare l'applicazione desktop" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Per usare lo sblocco biometrico, aggiornare l'applicazione desktop o disabilitare lo sblocco dell'impronta digitale nelle impostazioni del desktop." } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 1824c9314ee..2a6910f95bf 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "本人確認を行う" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "保管庫がロックされています。続行するには本人確認を行ってください。" }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "保管庫にアイテムが見つからない場合は、アイテムを追加するよう要求します。ログインしているすべてのアカウントに適用されます。" }, - "showCardsInVaultView": { - "message": "保管庫ビューに自動入力の候補としてカードを表示する" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "タブページにカードを表示" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "自動入力を簡単にするために、タブページにカードアイテムを表示します" }, - "showIdentitiesInVaultView": { - "message": "保管庫ビューに自動入力の候補として ID を表示する" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "タブページに ID を表示" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "エクストラワイド" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 1f8f2766e7b..96ebdc75ce9 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index a7913bbfc9b..69f276b06d3 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index ce85c7ea820..9194d581046 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "ನಿಮ್ಮ ವಾಲ್ಟ್ ಲಾಕ್ ಆಗಿದೆ. ಮುಂದುವರೆಯಲು ನಿಮ್ಮ ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ಪರಿಶೀಲಿಸಿ." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index c264492e0d2..ffec7ebbd25 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "신원 확인" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "보관함이 잠겨 있습니다. 마스터 비밀번호를 입력하여 계속하세요." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "보관함에 항목이 없을 경우 추가하라는 메시지를 표시합니다. 모든 로그인된 계정에 적용됩니다." }, - "showCardsInVaultView": { - "message": "보관함 보기에서 카드 자동 완성 제안을 표시" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "탭 페이지에 카드 표시" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "간편한 자동완성을 위해 탭에 카드 항목들을 나열" }, - "showIdentitiesInVaultView": { - "message": "보관함 보기에서 신원들의 자동 완성 제안을 표시" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "탭 페이지에 신원들을 표시" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "매우 넓게" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index f76ae921425..b2699faafb6 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Patvirtinti tapatybę" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Jūsų saugykla užrakinta. Norėdami tęsti, patikrinkite pagrindinį slaptažodį." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Paprašykite pridėti elementą, jei jo nerasta Jūsų saugykloje. Taikoma visoms prisijungusioms paskyroms." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Rodyti korteles skirtuko puslapyje" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Pateikti kortelių elementų skirtuko puslapyje sąrašą, kad būtų lengva automatiškai užpildyti." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Rodyti tapatybes skirtuko puslapyje" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index d18fbb92039..1417cb559ae 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Identitātes apliecināšana" }, + "weDontRecognizeThisDevice": { + "message": "Mēs neatpazīstam šo ierīci. Jāievada kods, kas tika nosūtīts e-pastā, lai apliecinātu savu identitāti." + }, + "continueLoggingIn": { + "message": "Turpināt pieteikšanos" + }, "yourVaultIsLocked": { "message": "Glabātava ir aizslēgta. Jāapliecina sava identitāte, lai turpinātu." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Vaicāt, vai pievienot vienumu, ja glabātavā tāds nav atrodams. Attiecas uz visiem kontiem, kuri ir pieteikušies." }, - "showCardsInVaultView": { - "message": "Rādīt kartes kā automātiskās aizpildes ieteikumus glabātavas skatā" + "showCardsInVaultViewV2": { + "message": "Glabātavas skatā vienmēr rādīt kartes kā automātiskās aizpildes ieteikumus" }, "showCardsCurrentTab": { "message": "Rādīt kartes cilnes lapā" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Attēlot kartes ciļņu lapā vieglākai aizpildīšanai." }, - "showIdentitiesInVaultView": { - "message": "Rādīt identitātes kā automātiskās aizpildes ieteikumus glabātavas skatā" + "showIdentitiesInVaultViewV2": { + "message": "Glabātavas skatā vienmēr rādīt identitātes kā automātiskās aizpildes ieteikumus" }, "showIdentitiesCurrentTab": { "message": "Rādīt identitātes cilnes pārskatā" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Ļoti plats" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Lūgums atjaunināt darbvirsmas lietotni" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Lai izmantotu atslēgšanu ar biometriju, lūgums atjaunināt darbvirsmas lietotni vai atspējot atslēgšanu ar pirkstu nospiedumu darbvirsmas iestatījumos." } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index cfbb1972388..131e5a080a1 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "തങ്ങളുടെ വാൾട് പൂട്ടിയിരിക്കുന്നു. തുടരുന്നതിന് നിങ്ങളുടെ പ്രാഥമിക പാസ്‌വേഡ് പരിശോധിക്കുക." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 05d6034acec..394da61d138 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "ओळख सत्यापित करा" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "तुमची तिजोरीला कुलूप लावले आहे. पुढे जाण्यासाठी तुमची ओळख सत्यापित करा." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index a7913bbfc9b..69f276b06d3 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index df676d2a3ba..8cef85cc3ad 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -81,10 +81,10 @@ "message": "Et hint for hovedpassordet (valgfritt)" }, "joinOrganization": { - "message": "Join organization" + "message": "Bli med i organisasjonen" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "Bli med i $ORGANIZATIONNAME$", "placeholders": { "organizationName": { "content": "$1", @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Bekreft identitet" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Hvelvet ditt er låst. Kontroller hovedpassordet ditt for å fortsette." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Vis kort på fanesiden" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Vis kortelementer på fanesiden for lett auto-utfylling." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Vis identiteter på fanesiden" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Ekstra bred" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index a7913bbfc9b..69f276b06d3 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 8a70ea4548c..8c0434031da 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Identiteit verifiëren" }, + "weDontRecognizeThisDevice": { + "message": "We herkennen dit apparaat niet. Voer de code in die naar je e-mail is verzonden om je identiteit te verifiëren." + }, + "continueLoggingIn": { + "message": "Doorgaan met inloggen" + }, "yourVaultIsLocked": { "message": "Je kluis is vergrendeld. Bevestig je identiteit om door te gaan." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Vraag om een item toe te voegen als het niet is gevonden is je kluis. Dit geld voor alle ingelogde accounts." }, - "showCardsInVaultView": { - "message": "Kaarten als Autofill-suggesties in de kluisweergave weergeven" + "showCardsInVaultViewV2": { + "message": "Kaarten altijd als Autofill-suggesties in de kluisweergave weergeven" }, "showCardsCurrentTab": { "message": "Kaarten weergeven op tabpagina" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Kaartenitems weergeven op de tabpagina voor gemakkelijk automatisch invullen." }, - "showIdentitiesInVaultView": { - "message": "Identiteiten als Autofill-suggesties in de kluisweergave weergeven" + "showIdentitiesInVaultViewV2": { + "message": "Identiteiten altijd als Autofill-suggesties in de kluisweergave weergeven" }, "showIdentitiesCurrentTab": { "message": "Identiteiten weergeven op tabpagina" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra breed" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Werk je desktopapplicatie bij" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Als u biometrische gegevens wilt gebruiken, moet je de desktopapplicatie bijwerken of vingerafdrukontgrendeling uitschakelen in de instellingen van de desktopapplicatie." } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index a7913bbfc9b..69f276b06d3 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index a7913bbfc9b..69f276b06d3 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index b79acf95a1c..9260ed2577c 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -20,7 +20,7 @@ "message": "Utwórz konto" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Nowy użytkownik Bitwarden?" }, "logInWithPasskey": { "message": "Zaloguj się używając passkey" @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Zweryfikuj tożsamość" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Sejf jest zablokowany. Zweryfikuj swoją tożsamość, aby kontynuować." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Poproś o dodanie elementu, jeśli nie zostanie znaleziony w Twoim sejfie. Dotyczy wszystkich zalogowanych kont." }, - "showCardsInVaultView": { - "message": "Pokaż karty jako sugestie autouzupełniania w widoku sejfu" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Pokaż karty na stronie głównej" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Pokaż elementy karty na stronie głównej, aby ułatwić autouzupełnianie." }, - "showIdentitiesInVaultView": { - "message": "Pokaż tożsamości jako sugestie autouzupełniania w widoku sejfu" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Pokaż tożsamości na stronie głównej" @@ -1320,7 +1326,7 @@ "message": "Limit czasu uwierzytelniania" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "Upłynął limit czasu uwierzytelniania. Uruchom ponownie proces logowania." }, "enterVerificationCodeEmail": { "message": "Wpisz 6-cyfrowy kod weryfikacyjny, który został przesłany na adres $EMAIL$.", @@ -2325,7 +2331,7 @@ "description": "A category title describing the concept of web domains" }, "blockedDomains": { - "message": "Blocked domains" + "message": "Zablokowane domeny" }, "excludedDomains": { "message": "Wykluczone domeny" @@ -2340,10 +2346,10 @@ "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, "autofillBlockedNoticeV2": { - "message": "Autofill is blocked for this website." + "message": "Autouzupełnianie jest zablokowane dla tej witryny." }, "autofillBlockedNoticeGuidance": { - "message": "Change this in settings" + "message": "Zmień to w ustawieniach" }, "websiteItemLabel": { "message": "Strona internetowa $number$ (URI)", @@ -2364,7 +2370,7 @@ } }, "blockedDomainsSavedSuccess": { - "message": "Blocked domain changes saved" + "message": "Zmiany w zablokowanych domenach zapisane" }, "excludedDomainsSavedSuccess": { "message": "Zmiany w wykluczonych domenach zapisane" @@ -2568,7 +2574,7 @@ "message": "Aby wybrać plik za pomocą przeglądarki Safari, otwórz rozszerzenie w nowym oknie." }, "popOut": { - "message": "Pop out" + "message": "Odepnij" }, "sendFileCalloutHeader": { "message": "Zanim zaczniesz" @@ -2805,10 +2811,10 @@ "message": "Błąd" }, "decryptionError": { - "message": "Decryption error" + "message": "Błąd odszyfrowywania" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden nie mógł odszyfrować elementów sejfu wymienionych poniżej." }, "contactCSToAvoidDataLossPart1": { "message": "Contact customer success", @@ -3109,10 +3115,10 @@ "message": "Powiadomienie zostało wysłane na twoje urządzenie" }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + "message": "Upewnij się, że Twoje konto jest odblokowane, a unikalny identyfikator konta pasuje do drugiego urządzenia" }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "Zostaniesz powiadomiony po zatwierdzeniu prośby" }, "needAnotherOptionV1": { "message": "Potrzebujesz innego sposobu?" @@ -3976,10 +3982,10 @@ "message": "Passkey został usunięty" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "Sugestie autouzupełniania" }, "itemSuggestions": { - "message": "Suggested items" + "message": "Sugerowane elementy" }, "autofillSuggestionsTip": { "message": "Zapisz element logowania dla tej witryny, aby automatycznie wypełnić" @@ -4636,22 +4642,22 @@ "message": "Nie masz uprawnień do edycji tego elementu" }, "biometricsStatusHelptextUnlockNeeded": { - "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + "message": "Odblokowanie odciskiem palca jest niedostępne, ponieważ najpierw wymagane jest odblokowanie kodem PIN lub hasłem." }, "biometricsStatusHelptextHardwareUnavailable": { - "message": "Biometric unlock is currently unavailable." + "message": "Odblokowanie biometryczne jest obecnie niedostępne." }, "biometricsStatusHelptextAutoSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Odblokowanie biometryczne jest niedostępne z powodu nieprawidłowej konfiguracji plików systemowych." }, "biometricsStatusHelptextManualSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Odblokowanie biometryczne jest niedostępne z powodu nieprawidłowej konfiguracji plików systemowych." }, "biometricsStatusHelptextDesktopDisconnected": { - "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + "message": "Odblokowanie odciskiem palca jest niedostępne, ponieważ aplikacja desktopowa Bitwarden jest zamknięta." }, "biometricsStatusHelptextNotEnabledInDesktop": { - "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "message": "Odblokowanie biometryczne jest niedostępne, ponieważ nie jest włączone dla $EMAIL$ w aplikacji desktopowej Bitwarden.", "placeholders": { "email": { "content": "$1", @@ -4660,7 +4666,7 @@ } }, "biometricsStatusHelptextUnavailableReasonUnknown": { - "message": "Biometric unlock is currently unavailable for an unknown reason." + "message": "Odblokowanie biometryczne jest obecnie niedostępne z nieznanego powodu." }, "authenticating": { "message": "Uwierzytelnianie" @@ -4825,22 +4831,22 @@ "message": "Beta" }, "importantNotice": { - "message": "Important notice" + "message": "Ważna informacja" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "Skonfiguruj dwustopniowe logowanie" }, "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + "message": "Bitwarden wyśle kod na Twój adres e-mail w celu zweryfikowania logowania z nowych urządzeń, począwszy od lutego 2025 r." }, "newDeviceVerificationNoticeContentPage2": { - "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + "message": "Możesz skonfigurować dwustopniowe logowanie jako alternatywny sposób ochrony konta lub zmienić swój adres e-mail do którego masz dostęp." }, "remindMeLater": { - "message": "Remind me later" + "message": "Przypomnij mi później" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "Czy masz pewny dostęp do swojego adresu e-mail, $EMAIL$?", "placeholders": { "email": { "content": "$1", @@ -4849,16 +4855,16 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "Nie, nie mam" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "Tak, mam pewny dostęp do mojego adresu e-mail" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "Włącz dwustopniowe logowanie" }, "changeAcctEmail": { - "message": "Change account email" + "message": "Zmień adres e-mail konta" }, "extensionWidth": { "message": "Szerokość rozszerzenia" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Bardzo szerokie" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 0971c5e1bc5..41b9635ecf2 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Verificar Identidade" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Seu cofre está trancado. Verifique sua identidade para continuar." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Pedir para adicionar um item se um não for encontrado no seu cofre. Aplica-se a todas as contas logadas." }, - "showCardsInVaultView": { - "message": "Mostrar cartões como sugestões de preenchimento automático na exibição do Cofre" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Mostrar cartões em páginas com guias." @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Exibir itens de cartão em páginas com abas para simplificar o preenchimento automático" }, - "showIdentitiesInVaultView": { - "message": "Mostrar identifica como sugestões de preenchimento automático na exibição do Cofre" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Exibir Identidades na Aba Atual" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra Grande" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 663e337d01c..11495ed608f 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Verificar identidade" }, + "weDontRecognizeThisDevice": { + "message": "Não reconhecemos este dispositivo. Introduza o código enviado para o seu e-mail para verificar a sua identidade." + }, + "continueLoggingIn": { + "message": "Continuar a iniciar sessão" + }, "yourVaultIsLocked": { "message": "O seu cofre está bloqueado. Verifique a sua identidade para continuar." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Pedir para adicionar um item se não for encontrado um no seu cofre. Aplica-se a todas as contas com sessão iniciada." }, - "showCardsInVaultView": { - "message": "Mostrar cartões como sugestões de preenchimento automático na vista do cofre" + "showCardsInVaultViewV2": { + "message": "Mostrar sempre cartões como sugestões de preenchimento automático na vista do cofre" }, "showCardsCurrentTab": { "message": "Mostrar cartões na página Separador" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Listar itens de cartões na página Separador para facilitar o preenchimento automático." }, - "showIdentitiesInVaultView": { - "message": "Mostrar identidades como sugestões de preenchimento automático na vista do cofre" + "showIdentitiesInVaultViewV2": { + "message": "Mostrar sempre identidades como sugestões de preenchimento automático na vista do cofre" }, "showIdentitiesCurrentTab": { "message": "Mostrar identidades na página Separador" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Muito ampla" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Por favor, atualize a sua aplicação para computador" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Para utilizar o desbloqueio biométrico, atualize a sua aplicação para computador ou desative o desbloqueio por impressão digital nas definições dessa mesma app." } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 165c3749b53..029d5470bd2 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Verificare identitate" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Seiful dvs. este blocat. Verificați-vă identitatea pentru a continua." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Afișați cardurile pe pagina Filă" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Listați elementele cardului pe pagina Filă pentru a facilita completarea automată." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Afișați identitățile pe pagina Filă" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 5519737f16c..8259023f8b4 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Подтвердить личность" }, + "weDontRecognizeThisDevice": { + "message": "Мы не распознали это устройство. Введите код, отправленный на ваш email, чтобы подтвердить вашу личность." + }, + "continueLoggingIn": { + "message": "Продолжить вход" + }, "yourVaultIsLocked": { "message": "Ваше хранилище заблокировано. Подтвердите свою личность, чтобы продолжить" }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Запрос на добавление элемента, если он отсутствует в вашем хранилище. Применяется ко всем авторизованным аккаунтам." }, - "showCardsInVaultView": { - "message": "Показывать карты как предложение автозаполнения при просмотре Хранилище" + "showCardsInVaultViewV2": { + "message": "Всегда показывать карты как предложения автозаполнения при просмотре хранилища" }, "showCardsCurrentTab": { "message": "Показывать карты на вкладке" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Карты будут отображены на вкладке для удобного автозаполнения." }, - "showIdentitiesInVaultView": { - "message": "Показывать личности как предложение автозаполнения при просмотре Хранилище" + "showIdentitiesInVaultViewV2": { + "message": "Всегда показывать личности как предложения автозаполнения при просмотре хранилища" }, "showIdentitiesCurrentTab": { "message": "Показывать Личности на вкладке" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Очень широкое" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Пожалуйста, обновите приложение для компьютера" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Чтобы использовать биометрическую разблокировку, обновите приложение для компьютера или отключите разблокировку по отпечатку пальца в настройках компьютера." } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 801e088aca8..c32e18aa87b 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "අනන්යතාවය සත්යාපනය කරන්න" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "ඔබේ සුරක්ෂිතාගාරය අගුළු දමා ඇත. දිගටම කරගෙන යාමට ඔබේ අනන්යතාවය සත්යාපනය කරන්න." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 92d0e35a51c..8c48a509ad0 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Overiť identitu" }, + "weDontRecognizeThisDevice": { + "message": "Toto zariadenie nepoznáme. Na overenie vašej totožnosti zadajte kód, ktorý bol zaslaný na váš e-mail." + }, + "continueLoggingIn": { + "message": "Pokračovať v prihlasovaní" + }, "yourVaultIsLocked": { "message": "Váš trezor je uzamknutý. Ak chcete pokračovať, overte svoju identitu." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Požiada o pridanie položky, ak sa v trezore nenachádza. Platí pre všetky prihlásené účty." }, - "showCardsInVaultView": { - "message": "Zobraziť karty ako návrhy automatického vypĺňania v zobrazení trezora" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Zobraziť karty na stránke \"Aktuálna karta\"" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Zoznam položiek karty na stránke \"Aktuálna karta\" na jednoduché automatické vyplnenie." }, - "showIdentitiesInVaultView": { - "message": "Zobraziť identity ako návrhy automatického vypĺňania v zobrazení trezora" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Zobraziť identity na stránke \"Aktuálna karta\"" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra široké" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Aktualizujte desktopovú aplikáciu" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Ak chcete používať biometrické odomykanie, aktualizujte desktopovú aplikáciu alebo vypnite odomykanie odtlačkom prsta v nastaveniach desktopovej aplikácie." } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index d57d6ec5e57..e1866033377 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Preverjanje istovetnosti" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Vaš trezor je zaklenjen. Za nadaljevanje potrdite svojo identiteto." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Če predmeta ni v trezorju, ga je potrebno dodati. Velja za vse prijavljene račune." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Prikaži kartice na strani Zavihek" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Na strani Zavihek prikaži kartice za lažje samodejno izpoljnjevanje." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Prikaži identitete na strani Zavihek" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 80de0ba3c8e..4f6d6b096af 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Потврдите идентитет" }, + "weDontRecognizeThisDevice": { + "message": "Не препознајемо овај уређај. Унесите код послат на адресу ваше електронске поште да би сте потврдили ваш идентитет." + }, + "continueLoggingIn": { + "message": "Настави са пријављивањем" + }, "yourVaultIsLocked": { "message": "Сеф је закључан. Унесите главну лозинку за наставак." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Затражите да додате ставку ако она није пронађена у вашем сефу. Односи се на све пријављене налоге." }, - "showCardsInVaultView": { - "message": "Прикажите картице као предлоге за ауто-попуњавање у приказу сефа" + "showCardsInVaultViewV2": { + "message": "Увек приказуј картице као препоруке аутоматског попуњавања на приказу трезора" }, "showCardsCurrentTab": { "message": "Прикажи кредитне картице на страници картице" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Прикажи ставке кредитних картица на страници картице за лакше аутоматско допуњавање." }, - "showIdentitiesInVaultView": { - "message": "Прикажите идентитете као предлоге за ауто-попуњавање у приказу сефа" + "showIdentitiesInVaultViewV2": { + "message": "Увек приказуј идентитете као препоруке аутоматског попуњавања на приказу трезора" }, "showIdentitiesCurrentTab": { "message": "Прикажи идентитете на страници" @@ -2325,7 +2331,7 @@ "description": "A category title describing the concept of web domains" }, "blockedDomains": { - "message": "Blocked domains" + "message": "Блокирани домени" }, "excludedDomains": { "message": "Изузети домени" @@ -2337,13 +2343,13 @@ "message": "Bitwarden неће тражити да сачува податке за пријављивање за ове домене за све пријављене налоге. Морате освежити страницу да би промене ступиле на снагу." }, "blockedDomainsDesc": { - "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + "message": "Аутоматско попуњавање и сродне функције неће бити понуђене за ове веб сајтове. Морате освежити страницу да би се измене примениле." }, "autofillBlockedNoticeV2": { - "message": "Autofill is blocked for this website." + "message": "Аутоматско попуњавање је блокирано за овај веб сајт." }, "autofillBlockedNoticeGuidance": { - "message": "Change this in settings" + "message": "Промените ово у подешавањима" }, "websiteItemLabel": { "message": "Сајт $number$ (УРЛ)", @@ -2364,7 +2370,7 @@ } }, "blockedDomainsSavedSuccess": { - "message": "Blocked domain changes saved" + "message": "Измене блокираних домена су сачуване" }, "excludedDomainsSavedSuccess": { "message": "Изузете промене домена су сачуване" @@ -2805,10 +2811,10 @@ "message": "Грешка" }, "decryptionError": { - "message": "Decryption error" + "message": "Грешка при декрипцији" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden није могао да декриптује ставке из трезора наведене испод." }, "contactCSToAvoidDataLossPart1": { "message": "Contact customer success", @@ -3976,10 +3982,10 @@ "message": "Приступни кључ је уклоњен" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "Предлози аутоматског попуњавања" }, "itemSuggestions": { - "message": "Suggested items" + "message": "Предложене ставке" }, "autofillSuggestionsTip": { "message": "Сачувајте ставку за пријаву за ову локацију за ауто-попуњавање" @@ -4636,22 +4642,22 @@ "message": "Немате дозволу да уређујете ову ставку" }, "biometricsStatusHelptextUnlockNeeded": { - "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + "message": "Биометријско откључавање није доступно јер је пре тога потребно унети ПИН или лозинку за откључавање." }, "biometricsStatusHelptextHardwareUnavailable": { - "message": "Biometric unlock is currently unavailable." + "message": "Биометријско откључавање тренутно није доступно." }, "biometricsStatusHelptextAutoSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Биометријско откључавање није доступно због лоше подешених системских датотека." }, "biometricsStatusHelptextManualSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Биометријско откључавање није доступно због лоше подешених системских датотека." }, "biometricsStatusHelptextDesktopDisconnected": { - "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + "message": "Биометријско откључавање није доступно јер је Bitwarden апликација на рачунару угашена." }, "biometricsStatusHelptextNotEnabledInDesktop": { - "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "message": "Биометријско откључавање није доступно јер није омогућено за $EMAIL$ у Bitwarden апликацији на рачунару.", "placeholders": { "email": { "content": "$1", @@ -4660,7 +4666,7 @@ } }, "biometricsStatusHelptextUnavailableReasonUnknown": { - "message": "Biometric unlock is currently unavailable for an unknown reason." + "message": "Биометријско откључавање није доступно из непознатог разлога." }, "authenticating": { "message": "Аутентификација" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Врло широко" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Молим вас надоградите вашу апликацију на рачунару" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Да би сте користили биометријско откључавање, надоградите вашу апликацију на рачунару, или онемогућите откључавање отиском прста у подешавањима на рачунару." } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 1927f12bc27..1019e2e84c4 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Verifiera identitet" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Ditt valv är låst. Verifiera din identitet för att fortsätta." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Visa kort på fliksida" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Lista kortobjekt på fliksidan för enkel automatisk fyllning." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Visa identiteter på fliksidan" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index a7913bbfc9b..69f276b06d3 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index a8a2585f42f..689f2f24f1d 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "ยืนยันตัวตน" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "ตู้เซฟของคุณถูกล็อก ยืนยันตัวตนของคุณเพื่อดำเนินการต่อ" }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "แสดงการ์ดบนหน้าแท็บ" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "บัตรรายการในหน้าแท็บเพื่อให้ป้อนอัตโนมัติได้ง่าย" }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "แสดงตัวตนบนหน้าแท็บ" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 34ad0f14483..5d41c146ab4 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Kimliği doğrula" }, + "weDontRecognizeThisDevice": { + "message": "Bu cihazı tanıyamadık. Kimliğinizi doğrulamak için e-postanıza gönderilen kodu girin." + }, + "continueLoggingIn": { + "message": "Giriş yapmaya devam et" + }, "yourVaultIsLocked": { "message": "Kasanız kilitli. Devam etmek için kimliğinizi doğrulayın." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Kasanızda bulunmayan kayıtların eklenmesini isteyip istemediğinizi sorar. Oturum açmış tüm hesaplar için geçerlidir." }, - "showCardsInVaultView": { - "message": "Kasa görünümünde kartları otomatik doldurma önerisi olarak göster" + "showCardsInVaultViewV2": { + "message": "Kasa görünümünde kartları her zaman otomatik doldurma önerisi olarak göster" }, "showCardsCurrentTab": { "message": "Sekme sayfasında kartları göster" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Kolay otomatik doldurma için sekme sayfasında kartları listele." }, - "showIdentitiesInVaultView": { - "message": "Kasa görünümünde kimlikleri otomatik doldurma önerisi olarak göster" + "showIdentitiesInVaultViewV2": { + "message": "Kasa görünümünde kimlikleri her zaman otomatik doldurma önerisi olarak göster" }, "showIdentitiesCurrentTab": { "message": "Sekme sayfasında kimlikleri göster" @@ -1466,7 +1472,7 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Önerileri otomatik doldur" + "message": "Otomatik doldurma önerileri" }, "showInlineMenuLabel": { "message": "Form alanlarında otomatik doldurma önerilerini göster" @@ -3976,7 +3982,7 @@ "message": "Geçiş anahtarı kaldırıldı" }, "autofillSuggestions": { - "message": "Önerileri otomatik doldur" + "message": "Otomatik doldurma önerileri" }, "itemSuggestions": { "message": "Önerilen kayıtlar" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Ekstra geniş" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Lütfen masaüstü uygulamanızı güncelleyin" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index a1ec52ee80e..adb1ccc6719 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Виконати перевірку" }, + "weDontRecognizeThisDevice": { + "message": "Ми не розпізнаємо цей пристрій. Введіть код, надісланий на вашу електронну пошту, щоб підтвердити вашу особу." + }, + "continueLoggingIn": { + "message": "Продовжити вхід" + }, "yourVaultIsLocked": { "message": "Ваше сховище заблоковане. Для продовження виконайте перевірку." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Запитувати про додавання запису, якщо такого не знайдено у вашому сховищі. Застосовується для всіх облікових записів, до яких виконано вхід." }, - "showCardsInVaultView": { - "message": "Показувати картки як пропозиції автозаповнення в режимі перегляду сховища" + "showCardsInVaultViewV2": { + "message": "Завжди показувати картки як пропозиції автозаповнення в режимі перегляду сховища" }, "showCardsCurrentTab": { "message": "Показувати картки на вкладці" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Показувати список карток на сторінці вкладки для легкого автозаповнення." }, - "showIdentitiesInVaultView": { - "message": "Показувати посвідчення як пропозиції автозаповнення в режимі перегляду сховища" + "showIdentitiesInVaultViewV2": { + "message": "Завжди показувати посвідчення як пропозиції автозаповнення в режимі перегляду сховища" }, "showIdentitiesCurrentTab": { "message": "Показувати посвідчення на вкладці" @@ -2391,7 +2397,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDetails": { - "message": "Надіслати подробиці", + "message": "Подробиці відправлення", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeText": { @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Дуже широке" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Оновіть свою комп'ютерну програму" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Щоб використовувати біометричне розблокування, оновіть комп'ютерну програму, або вимкніть розблокування відбитком пальця в налаштуваннях системи." } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index b6668d8dc99..eb5075893cd 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "Xác minh danh tính" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Kho của bạn đã bị khóa. Xác minh danh tính của bạn để mở khoá." }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "Đưa ra lựa chọn để thêm một mục nếu không tìm thấy mục đó trong hòm của bạn. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, - "showCardsInVaultView": { - "message": "Hiển thị các thẻ như các gợi ý tự động điền trên giao diện kho" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Hiển thị thẻ trên trang Tab" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "Liệt kê các mục thẻ trên trang Tab để dễ dàng tự động điền." }, - "showIdentitiesInVaultView": { - "message": "Hiển thị các danh tính như các gợi ý tự động điền trên giao diện kho" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Hiển thị danh tính trên trang Tab" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 8cd91d44a8a..ba4058b4a89 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "验证身份" }, + "weDontRecognizeThisDevice": { + "message": "我们无法识别这个设备。请输入发送到您电子邮箱中的代码以验证您的身份。" + }, + "continueLoggingIn": { + "message": "继续登录" + }, "yourVaultIsLocked": { "message": "您的密码库已锁定。请先验证您的身份。" }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "如果在密码库中找不到项目,询问添加一个。适用于所有已登录的账户。" }, - "showCardsInVaultView": { - "message": "在密码库视图中将支付卡显示为自动填充建议" + "showCardsInVaultViewV2": { + "message": "在密码库视图中将支付卡始终显示为自动填充建议" }, "showCardsCurrentTab": { "message": "在标签页上显示支付卡" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "在标签页上列出支付卡项目,以便于自动填充。" }, - "showIdentitiesInVaultView": { - "message": "在密码库视图中将身份显示为自动填充建议" + "showIdentitiesInVaultViewV2": { + "message": "在密码库视图中将身份始终显示为自动填充建议" }, "showIdentitiesCurrentTab": { "message": "在标签页上显示身份" @@ -2325,7 +2331,7 @@ "description": "A category title describing the concept of web domains" }, "blockedDomains": { - "message": "屏蔽域名" + "message": "屏蔽的域名" }, "excludedDomains": { "message": "排除域名" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "超宽" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "请更新您的桌面应用程序" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "要使用生物识别解锁,请更新您的桌面应用程序,或在桌面设置中禁用指纹解锁。" } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 13edf4920de..8f38bf44f7f 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -647,6 +647,12 @@ "verifyIdentity": { "message": "驗證身份" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "您的密碼庫已鎖定。請驗證身分以繼續。" }, @@ -986,8 +992,8 @@ "addLoginNotificationDescAlt": { "message": "如果在您的密碼庫中找不到項目,則詢問是否新增項目。適用於所有已登入的帳戶。" }, - "showCardsInVaultView": { - "message": "在密碼庫介面中顯示支付卡自動填入建議" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "於分頁頁面顯示支付卡" @@ -995,8 +1001,8 @@ "showCardsCurrentTabDesc": { "message": "於分頁頁面顯示信用卡以便於自動填入。" }, - "showIdentitiesInVaultView": { - "message": "在密碼庫介面中顯示身分自動填入建議" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "於分頁頁面顯示身分" @@ -4868,5 +4874,11 @@ }, "extraWide": { "message": "更寬" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "請更新您的桌面應用程式" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "為了使用生物辨識解鎖,請更新您的桌面應用程式,或在設定中停用指紋解鎖。" } } diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts index a2f9cd9d0fc..43230bd23f4 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -33,7 +33,7 @@ import { BrowserApi } from "../../platform/browser/browser-api"; import { ZonedMessageListenerService } from "../../platform/browser/zoned-message-listener.service"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; -import { closeTwoFactorAuthPopout } from "./utils/auth-popout-window"; +import { closeTwoFactorAuthWebAuthnPopout } from "./utils/auth-popout-window"; @Component({ selector: "app-two-factor", @@ -171,7 +171,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnInit // We don't need this window anymore because the intent is for the user to be left // on the web vault screen which tells them to continue in the browser extension (sidebar or popup) - await closeTwoFactorAuthPopout(); + await closeTwoFactorAuthWebAuthnPopout(); }; } }); diff --git a/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts b/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts index 9e7d69fad97..deb71f73cd6 100644 --- a/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts +++ b/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts @@ -7,8 +7,9 @@ import { openUnlockPopout, closeUnlockPopout, openSsoAuthResultPopout, - openTwoFactorAuthPopout, - closeTwoFactorAuthPopout, + openTwoFactorAuthWebAuthnPopout, + closeTwoFactorAuthWebAuthnPopout, + closeSsoAuthResultPopout, } from "./auth-popout-window"; describe("AuthPopoutWindow", () => { @@ -97,22 +98,30 @@ describe("AuthPopoutWindow", () => { }); }); - describe("openTwoFactorAuthPopout", () => { - it("opens a window that facilitates two factor authentication", async () => { - await openTwoFactorAuthPopout({ data: "data", remember: "remember" }); + describe("closeSsoAuthResultPopout", () => { + it("closes the SSO authentication result popout window", async () => { + await closeSsoAuthResultPopout(); + + expect(closeSingleActionPopoutSpy).toHaveBeenCalledWith(AuthPopoutType.ssoAuthResult); + }); + }); + + describe("openTwoFactorAuthWebAuthnPopout", () => { + it("opens a window that facilitates two factor authentication via WebAuthn", async () => { + await openTwoFactorAuthWebAuthnPopout({ data: "data", remember: "remember" }); expect(openPopoutSpy).toHaveBeenCalledWith( "popup/index.html#/2fa;webAuthnResponse=data;remember=remember", - { singleActionKey: AuthPopoutType.twoFactorAuth }, + { singleActionKey: AuthPopoutType.twoFactorAuthWebAuthn }, ); }); }); - describe("closeTwoFactorAuthPopout", () => { - it("closes the two-factor authentication window", async () => { - await closeTwoFactorAuthPopout(); + describe("closeTwoFactorAuthWebAuthnPopout", () => { + it("closes the two-factor authentication WebAuthn window", async () => { + await closeTwoFactorAuthWebAuthnPopout(); - expect(closeSingleActionPopoutSpy).toHaveBeenCalledWith(AuthPopoutType.twoFactorAuth); + expect(closeSingleActionPopoutSpy).toHaveBeenCalledWith(AuthPopoutType.twoFactorAuthWebAuthn); }); }); }); diff --git a/apps/browser/src/auth/popup/utils/auth-popout-window.ts b/apps/browser/src/auth/popup/utils/auth-popout-window.ts index 5a0e577807f..8d6e7fa92cd 100644 --- a/apps/browser/src/auth/popup/utils/auth-popout-window.ts +++ b/apps/browser/src/auth/popup/utils/auth-popout-window.ts @@ -6,7 +6,7 @@ import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; const AuthPopoutType = { unlockExtension: "auth_unlockExtension", ssoAuthResult: "auth_ssoAuthResult", - twoFactorAuth: "auth_twoFactorAuth", + twoFactorAuthWebAuthn: "auth_twoFactorAuthWebAuthn", } as const; const extensionUnlockUrls = new Set([ chrome.runtime.getURL("popup/index.html#/lock"), @@ -60,26 +60,37 @@ async function openSsoAuthResultPopout(resultData: { code: string; state: string } /** - * Opens a window that facilitates two-factor authentication. + * Closes the SSO authentication result popout window. + */ +async function closeSsoAuthResultPopout() { + await BrowserPopupUtils.closeSingleActionPopout(AuthPopoutType.ssoAuthResult); +} + +/** + * Opens a popout that facilitates two-factor authentication via WebAuthn. * - * @param twoFactorAuthData - The data from the two-factor authentication. + * @param twoFactorAuthWebAuthnData - The data to send ot the popout via query param. + * It includes the WebAuthn response and whether to save the 2FA remember me token or not. */ -async function openTwoFactorAuthPopout(twoFactorAuthData: { data: string; remember: string }) { - const { data, remember } = twoFactorAuthData; +async function openTwoFactorAuthWebAuthnPopout(twoFactorAuthWebAuthnData: { + data: string; + remember: string; +}) { + const { data, remember } = twoFactorAuthWebAuthnData; const params = `webAuthnResponse=${encodeURIComponent(data)};` + `remember=${encodeURIComponent(remember)}`; const twoFactorUrl = `popup/index.html#/2fa;${params}`; await BrowserPopupUtils.openPopout(twoFactorUrl, { - singleActionKey: AuthPopoutType.twoFactorAuth, + singleActionKey: AuthPopoutType.twoFactorAuthWebAuthn, }); } /** * Closes the two-factor authentication popout window. */ -async function closeTwoFactorAuthPopout() { - await BrowserPopupUtils.closeSingleActionPopout(AuthPopoutType.twoFactorAuth); +async function closeTwoFactorAuthWebAuthnPopout() { + await BrowserPopupUtils.closeSingleActionPopout(AuthPopoutType.twoFactorAuthWebAuthn); } export { @@ -87,6 +98,7 @@ export { openUnlockPopout, closeUnlockPopout, openSsoAuthResultPopout, - openTwoFactorAuthPopout, - closeTwoFactorAuthPopout, + closeSsoAuthResultPopout, + openTwoFactorAuthWebAuthnPopout, + closeTwoFactorAuthWebAuthnPopout, }; diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.html b/apps/browser/src/autofill/popup/settings/autofill.component.html index eeae0a85e3f..e8299f01166 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.html +++ b/apps/browser/src/autofill/popup/settings/autofill.component.html @@ -118,7 +118,7 @@

{{ "autofillSuggestionsSectionTitle" | i18n }}

(change)="updateShowCardsCurrentTab()" [(ngModel)]="showCardsCurrentTab" /> - {{ "showCardsInVaultView" | i18n }} + {{ "showCardsInVaultViewV2" | i18n }} {{ "autofillSuggestionsSectionTitle" | i18n }} [(ngModel)]="showIdentitiesCurrentTab" /> - {{ "showIdentitiesInVaultView" | i18n }} + {{ "showIdentitiesInVaultViewV2" | i18n }} diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts index f15c3e4c389..7471c298917 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts @@ -21,7 +21,7 @@ jest.mock("../utils", () => { const utils = jest.requireActual("../utils"); return { ...utils, - debounce: jest.fn((fn, wait) => setTimeout(() => fn(), wait)), + debounce: jest.fn((fn) => fn), }; }); diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.ts index 8ccf726aa69..b858af25fae 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -947,8 +947,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ } if (!this.mutationsQueue.length) { - // Collect all mutations and debounce the processing of those mutations by 100ms to ensure we don't process too many mutations at once. - debounce(this.processMutations, 100); + requestIdleCallbackPolyfill(debounce(this.processMutations, 100), { timeout: 500 }); } this.mutationsQueue.push(mutations); }; diff --git a/apps/browser/src/autofill/utils/index.ts b/apps/browser/src/autofill/utils/index.ts index 0e102dcfd99..614a5b014f2 100644 --- a/apps/browser/src/autofill/utils/index.ts +++ b/apps/browser/src/autofill/utils/index.ts @@ -37,7 +37,9 @@ export function requestIdleCallbackPolyfill( return globalThis.requestIdleCallback(() => callback(), options); } - return globalThis.setTimeout(() => callback(), 1); + const timeoutDelay = options?.timeout || 1; + + return globalThis.setTimeout(() => callback(), timeoutDelay); } /** diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index b26360407e1..192455f9691 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -92,6 +92,7 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { @@ -125,6 +126,7 @@ import { FileUploadService } from "@bitwarden/common/platform/services/file-uplo import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; +import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory"; import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service"; import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory"; import { StateService } from "@bitwarden/common/platform/services/state.service"; @@ -260,7 +262,7 @@ import { LocalBackedSessionStorageService } from "../platform/services/local-bac import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; import { PopupViewCacheBackgroundService } from "../platform/services/popup-view-cache-background.service"; -import { BrowserSdkClientFactory } from "../platform/services/sdk/browser-sdk-client-factory"; +import { BrowserSdkLoadService } from "../platform/services/sdk/browser-sdk-load.service"; import { BackgroundTaskSchedulerService } from "../platform/services/task-scheduler/background-task-scheduler.service"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; import { BrowserStorageServiceProvider } from "../platform/storage/browser-storage-service.provider"; @@ -379,6 +381,7 @@ export default class MainBackground { themeStateService: DefaultThemeStateService; autoSubmitLoginBackground: AutoSubmitLoginBackground; sdkService: SdkService; + sdkLoadService: SdkLoadService; cipherAuthorizationService: CipherAuthorizationService; inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; @@ -730,8 +733,9 @@ export default class MainBackground { ); const sdkClientFactory = flagEnabled("sdk") - ? new BrowserSdkClientFactory(this.logService) + ? new DefaultSdkClientFactory() : new NoopSdkClientFactory(); + this.sdkLoadService = new BrowserSdkLoadService(this.logService); this.sdkService = new DefaultSdkService( sdkClientFactory, this.environmentService, @@ -1257,6 +1261,7 @@ export default class MainBackground { async bootstrap() { this.containerService.attachToGlobal(self); + await this.sdkLoadService.load(); // Only the "true" background should run migrations await this.stateService.init({ runMigrations: true }); diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 38bb2ec50c9..2f038946bf9 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -22,7 +22,7 @@ import { BiometricsCommands } from "@bitwarden/key-management"; import { closeUnlockPopout, openSsoAuthResultPopout, - openTwoFactorAuthPopout, + openTwoFactorAuthWebAuthnPopout, } from "../auth/popup/utils/auth-popout-window"; import { LockedVaultPendingNotificationsData } from "../autofill/background/abstractions/notification.background"; import { AutofillService } from "../autofill/services/abstractions/autofill.service"; @@ -333,7 +333,7 @@ export default class RuntimeBackground { return; } - await openTwoFactorAuthPopout(msg); + await openTwoFactorAuthWebAuthnPopout(msg); break; } case "reloadPopup": diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 016bf6dfe4b..90ae271fd14 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2025.1.1", + "version": "2025.1.4", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 104036140bd..988b6eeb9c5 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2025.1.1", + "version": "2025.1.4", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts b/apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts similarity index 55% rename from apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts rename to apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts index 0499f34a4ae..ca41127407c 100644 --- a/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts +++ b/apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts @@ -1,11 +1,12 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; -import type { BitwardenClient } from "@bitwarden/sdk-internal"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { BrowserApi } from "../../browser/browser-api"; +export type GlobalWithWasmInit = typeof globalThis & { + initSdk: () => void; +}; + // https://stackoverflow.com/a/47880734 const supported = (() => { try { @@ -17,9 +18,7 @@ const supported = (() => { return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; } } - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e) { + } catch { // ignore } return false; @@ -33,54 +32,42 @@ let loadingPromise: Promise | undefined; if (BrowserApi.isManifestVersion(3)) { if (supported) { // eslint-disable-next-line no-console - console.debug("WebAssembly is supported in this environment"); + console.info("WebAssembly is supported in this environment"); loadingPromise = import("./wasm"); } else { // eslint-disable-next-line no-console - console.debug("WebAssembly is not supported in this environment"); + console.info("WebAssembly is not supported in this environment"); loadingPromise = import("./fallback"); } } // Manifest v2 expects dynamic imports to prevent timing issues. -async function load() { +async function importModule(): Promise { if (BrowserApi.isManifestVersion(3)) { // Ensure we have loaded the module await loadingPromise; - return; - } - - if (supported) { + } else if (supported) { // eslint-disable-next-line no-console - console.debug("WebAssembly is supported in this environment"); + console.info("WebAssembly is supported in this environment"); await import("./wasm"); } else { // eslint-disable-next-line no-console - console.debug("WebAssembly is not supported in this environment"); + console.info("WebAssembly is not supported in this environment"); await import("./fallback"); } + + // the wasm and fallback imports mutate globalThis to add the initSdk function + return (globalThis as GlobalWithWasmInit).initSdk; } -/** - * SDK client factory with a js fallback for when WASM is not supported. - * - * Works both in popup and service worker. - */ -export class BrowserSdkClientFactory implements SdkClientFactory { - constructor(private logService: LogService) {} +export class BrowserSdkLoadService implements SdkLoadService { + constructor(readonly logService: LogService) {} - async createSdkClient( - ...args: ConstructorParameters - ): Promise { + async load(): Promise { const startTime = performance.now(); - await load(); - + await importModule().then((initSdk) => initSdk()); const endTime = performance.now(); - const instance = (globalThis as any).init_sdk(...args); - - this.logService.info("WASM SDK loaded in", Math.round(endTime - startTime), "ms"); - - return instance; + this.logService.info(`WASM SDK loaded in ${Math.round(endTime - startTime)}ms`); } } diff --git a/apps/browser/src/platform/services/sdk/fallback.ts b/apps/browser/src/platform/services/sdk/fallback.ts index 82d292fc9ee..cee3598feda 100644 --- a/apps/browser/src/platform/services/sdk/fallback.ts +++ b/apps/browser/src/platform/services/sdk/fallback.ts @@ -1,8 +1,8 @@ import * as sdk from "@bitwarden/sdk-internal"; import * as wasm from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm.js"; -(globalThis as any).init_sdk = (...args: ConstructorParameters) => { - (sdk as any).init(wasm); +import { GlobalWithWasmInit } from "./browser-sdk-load.service"; - return new sdk.BitwardenClient(...args); +(globalThis as GlobalWithWasmInit).initSdk = () => { + (sdk as any).init(wasm); }; diff --git a/apps/browser/src/platform/services/sdk/wasm.ts b/apps/browser/src/platform/services/sdk/wasm.ts index 1977a171e23..de2eeffd294 100644 --- a/apps/browser/src/platform/services/sdk/wasm.ts +++ b/apps/browser/src/platform/services/sdk/wasm.ts @@ -1,8 +1,8 @@ import * as sdk from "@bitwarden/sdk-internal"; import * as wasm from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"; -(globalThis as any).init_sdk = (...args: ConstructorParameters) => { - (sdk as any).init(wasm); +import { GlobalWithWasmInit } from "./browser-sdk-load.service"; - return new sdk.BitwardenClient(...args); +(globalThis as GlobalWithWasmInit).initSdk = () => { + (sdk as any).init(wasm); }; diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index b831eef0baa..9473dc63bad 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -1,21 +1,23 @@ import { Injectable, NgModule } from "@angular/core"; import { ActivatedRouteSnapshot, RouteReuseStrategy, RouterModule, Routes } from "@angular/router"; +import { AuthenticationTimeoutComponent } from "@bitwarden/angular/auth/components/authentication-timeout.component"; import { EnvironmentSelectorComponent, EnvironmentSelectorRouteData, ExtensionDefaultOverlayPosition, } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { TwoFactorTimeoutComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-expired.component"; import { unauthUiRefreshRedirect } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-redirect"; import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap"; import { authGuard, lockGuard, + activeAuthGuard, redirectGuard, tdeDecryptionRequiredGuard, unauthGuardFn, } from "@bitwarden/angular/auth/guards"; +import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { twofactorRefactorSwap } from "@bitwarden/angular/utils/two-factor-component-refactor-route-swap"; import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { @@ -39,7 +41,10 @@ import { DevicesIcon, SsoComponent, TwoFactorTimeoutIcon, + NewDeviceVerificationComponent, + DeviceVerificationIcon, } from "@bitwarden/auth/angular"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { LockComponent } from "@bitwarden/key-management/angular"; import { NewDeviceVerificationNoticePageOneComponent, @@ -172,12 +177,12 @@ const routes: Routes = [ component: ExtensionAnonLayoutWrapperComponent, children: [ { - path: "2fa-timeout", + path: "authentication-timeout", canActivate: [unauthGuardFn(unauthRouteOverrides)], children: [ { path: "", - component: TwoFactorTimeoutComponent, + component: AuthenticationTimeoutComponent, }, ], data: { @@ -230,6 +235,27 @@ const routes: Routes = [ ], }, ), + { + path: "device-verification", + component: ExtensionAnonLayoutWrapperComponent, + canActivate: [ + canAccessFeature(FeatureFlag.NewDeviceVerification), + unauthGuardFn(), + activeAuthGuard(), + ], + children: [{ path: "", component: NewDeviceVerificationComponent }], + data: { + pageIcon: DeviceVerificationIcon, + pageTitle: { + key: "verifyIdentity", + }, + pageSubtitle: { + key: "weDontRecognizeThisDevice", + }, + showBackButton: true, + elevation: 1, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, + }, { path: "set-password", component: SetPasswordComponent, diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index 24661438495..069ebf4020d 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -6,6 +6,7 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BrowserApi } from "../../platform/browser/browser-api"; @@ -22,11 +23,13 @@ export class InitService { private twoFactorService: TwoFactorService, private logService: LogServiceAbstraction, private themingService: AbstractThemingService, + private sdkLoadService: SdkLoadService, @Inject(DOCUMENT) private document: Document, ) {} init() { return async () => { + await this.sdkLoadService.load(); await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations await this.i18nService.init(); this.twoFactorService.init(); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 20cd1d05b05..67ed8391de3 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -70,6 +70,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService, @@ -82,6 +83,7 @@ import { flagEnabled } from "@bitwarden/common/platform/misc/flags"; import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory"; import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; @@ -144,7 +146,7 @@ import BrowserMemoryStorageService from "../../platform/services/browser-memory- import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; -import { BrowserSdkClientFactory } from "../../platform/services/sdk/browser-sdk-client-factory"; +import { BrowserSdkLoadService } from "../../platform/services/sdk/browser-sdk-load.service"; import { ForegroundTaskSchedulerService } from "../../platform/services/task-scheduler/foreground-task-scheduler.service"; import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; @@ -566,11 +568,16 @@ const safeProviders: SafeProvider[] = [ deps: [MessageSender, MessageListener], }), safeProvider({ - provide: SdkClientFactory, - useFactory: (logService: LogService) => - flagEnabled("sdk") ? new BrowserSdkClientFactory(logService) : new NoopSdkClientFactory(), + provide: SdkLoadService, + useClass: BrowserSdkLoadService, deps: [LogService], }), + safeProvider({ + provide: SdkClientFactory, + useFactory: () => + flagEnabled("sdk") ? new DefaultSdkClientFactory() : new NoopSdkClientFactory(), + deps: [], + }), safeProvider({ provide: LoginEmailService, useClass: LoginEmailService, diff --git a/apps/browser/src/services/families-policy.service.ts b/apps/browser/src/services/families-policy.service.ts index 887e8836953..755d3e84591 100644 --- a/apps/browser/src/services/families-policy.service.ts +++ b/apps/browser/src/services/families-policy.service.ts @@ -47,7 +47,7 @@ export class FamiliesPolicyService { map((organizations) => organizations.find((org) => org.canManageSponsorships)?.id), switchMap((enterpriseOrgId) => this.policyService - .getAll$(PolicyType.FreeFamiliesSponsorshipPolicy) + .getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId) .pipe( map( (policies) => diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html index a46f5a6955b..152c500d6ca 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html @@ -26,5 +26,15 @@ + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts index ebfb1ff765f..3252f030fc3 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts @@ -1,17 +1,19 @@ import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; import { ActivatedRoute, Router } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, Observable } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventType } from "@bitwarden/common/enums"; 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info"; import { CipherFormConfig, @@ -40,7 +42,7 @@ describe("AddEditV2Component", () => { const buildConfigResponse = { originalCipher: {} } as CipherFormConfig; const buildConfig = jest.fn((mode: CipherFormMode) => - Promise.resolve({ mode, ...buildConfigResponse }), + Promise.resolve({ ...buildConfigResponse, mode }), ); const queryParams$ = new BehaviorSubject({}); const disable = jest.fn(); @@ -55,9 +57,10 @@ describe("AddEditV2Component", () => { back.mockClear(); collect.mockClear(); - addEditCipherInfo$ = new BehaviorSubject(null); + addEditCipherInfo$ = new BehaviorSubject(null); cipherServiceMock = mock(); - cipherServiceMock.addEditCipherInfo$ = addEditCipherInfo$.asObservable(); + cipherServiceMock.addEditCipherInfo$ = + addEditCipherInfo$.asObservable() as Observable; await TestBed.configureTestingModule({ imports: [AddEditV2Component], @@ -71,6 +74,13 @@ describe("AddEditV2Component", () => { { provide: I18nService, useValue: { t: (key: string) => key } }, { provide: CipherService, useValue: cipherServiceMock }, { provide: EventCollectionService, useValue: { collect } }, + { provide: LogService, useValue: mock() }, + { + provide: CipherAuthorizationService, + useValue: { + canDeleteCipher$: jest.fn().mockReturnValue(true), + }, + }, ], }) .overrideProvider(CipherFormConfigService, { @@ -92,7 +102,7 @@ describe("AddEditV2Component", () => { tick(); - expect(buildConfig.mock.lastCall[0]).toBe("add"); + expect(buildConfig.mock.lastCall![0]).toBe("add"); expect(component.config.mode).toBe("add"); })); @@ -101,7 +111,7 @@ describe("AddEditV2Component", () => { tick(); - expect(buildConfig.mock.lastCall[0]).toBe("clone"); + expect(buildConfig.mock.lastCall![0]).toBe("clone"); expect(component.config.mode).toBe("clone"); })); @@ -111,7 +121,7 @@ describe("AddEditV2Component", () => { tick(); - expect(buildConfig.mock.lastCall[0]).toBe("edit"); + expect(buildConfig.mock.lastCall![0]).toBe("edit"); expect(component.config.mode).toBe("edit"); })); @@ -121,7 +131,7 @@ describe("AddEditV2Component", () => { tick(); - expect(buildConfig.mock.lastCall[0]).toBe("edit"); + expect(buildConfig.mock.lastCall![0]).toBe("edit"); expect(component.config.mode).toBe("partial-edit"); })); }); @@ -218,7 +228,7 @@ describe("AddEditV2Component", () => { tick(); - expect(component.config.initialValues.username).toBe("identity-username"); + expect(component.config.initialValues!.username).toBe("identity-username"); })); it("overrides query params with `addEditCipherInfo` values", fakeAsync(() => { @@ -231,7 +241,7 @@ describe("AddEditV2Component", () => { tick(); - expect(component.config.initialValues.name).toBe("AddEditCipherName"); + expect(component.config.initialValues!.name).toBe("AddEditCipherName"); })); it("clears `addEditCipherInfo` after initialization", fakeAsync(() => { @@ -326,4 +336,30 @@ describe("AddEditV2Component", () => { expect(back).toHaveBeenCalled(); }); }); + + describe("delete", () => { + it("dialogService openSimpleDialog called when deleteBtn is hit", async () => { + const dialogSpy = jest + .spyOn(component["dialogService"], "openSimpleDialog") + .mockResolvedValue(true); + + await component.delete(); + expect(dialogSpy).toHaveBeenCalled(); + }); + + it("should call deleteCipher when user confirms deletion", async () => { + const deleteCipherSpy = jest.spyOn(component as any, "deleteCipher"); + jest.spyOn(component["dialogService"], "openSimpleDialog").mockResolvedValue(true); + + await component.delete(); + expect(deleteCipherSpy).toHaveBeenCalled(); + }); + + it("navigates to vault tab after deletion", async () => { + jest.spyOn(component["dialogService"], "openSimpleDialog").mockResolvedValue(true); + await component.delete(); + + expect(navigate).toHaveBeenCalledWith(["/tabs/vault"]); + }); + }); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts index 2d8c4857c1c..b46b1d61509 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts @@ -5,18 +5,27 @@ import { Component, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormsModule } from "@angular/forms"; import { ActivatedRoute, Params, Router } from "@angular/router"; -import { firstValueFrom, map, switchMap } from "rxjs"; +import { firstValueFrom, map, Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info"; -import { AsyncActionsModule, ButtonModule, SearchModule } from "@bitwarden/components"; +import { + AsyncActionsModule, + ButtonModule, + SearchModule, + IconButtonModule, + DialogService, + ToastService, +} from "@bitwarden/components"; import { CipherFormConfig, CipherFormConfigService, @@ -131,11 +140,13 @@ export type AddEditQueryParams = Partial>; CipherFormModule, AsyncActionsModule, PopOutComponent, + IconButtonModule, ], }) export class AddEditV2Component implements OnInit { headerText: string; config: CipherFormConfig; + canDeleteCipher$: Observable; get loading() { return this.config == null; @@ -165,6 +176,10 @@ export class AddEditV2Component implements OnInit { private router: Router, private cipherService: CipherService, private eventCollectionService: EventCollectionService, + private logService: LogService, + private toastService: ToastService, + private dialogService: DialogService, + protected cipherAuthorizationService: CipherAuthorizationService, ) { this.subscribeToParams(); } @@ -281,6 +296,10 @@ export class AddEditV2Component implements OnInit { } if (["edit", "partial-edit"].includes(config.mode) && config.originalCipher?.id) { + this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$( + config.originalCipher, + ); + await this.eventCollectionService.collect( EventType.Cipher_ClientViewed, config.originalCipher.id, @@ -337,6 +356,43 @@ export class AddEditV2Component implements OnInit { return this.i18nService.t(partOne, this.i18nService.t("typeSshKey")); } } + + delete = async () => { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "deleteItem" }, + content: { + key: "deleteItemConfirmation", + }, + type: "warning", + }); + + if (!confirmed) { + return false; + } + + try { + await this.deleteCipher(); + } catch (e) { + this.logService.error(e); + return false; + } + + await this.router.navigate(["/tabs/vault"]); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedItem"), + }); + + return true; + }; + + protected deleteCipher() { + return this.config.originalCipher.deletedDate + ? this.cipherService.deleteWithServer(this.config.originalCipher.id) + : this.cipherService.softDeleteWithServer(this.config.originalCipher.id); + } } /** diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html index c55e8d9fb26..1593c747f7d 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html @@ -91,6 +91,7 @@

{{ cipher.name }} { - const showCards = showCardsSettingEnabled && nonLoginCipherTypesOnPage[CipherType.Card]; + const showCards = showCardsSettingEnabled || nonLoginCipherTypesOnPage[CipherType.Card]; const showIdentities = - showIdentitiesSettingEnabled && nonLoginCipherTypesOnPage[CipherType.Identity]; + showIdentitiesSettingEnabled || nonLoginCipherTypesOnPage[CipherType.Identity]; return [ ...(showCards ? [CipherType.Card] : []), diff --git a/apps/browser/store/locales/nb/copy.resx b/apps/browser/store/locales/nb/copy.resx index b496e223cbe..29d612906c7 100644 --- a/apps/browser/store/locales/nb/copy.resx +++ b/apps/browser/store/locales/nb/copy.resx @@ -118,58 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden passordbehandler + Bitwarden Passordbehandler - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Hjemme, på jobben eller på farten sikrer Bitwarden enkelt alle passordene dine, passordene og sensitiv informasjon. - Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + Anerkjent som den beste passordbehandleren av PCMag, WIRED, The Verge, CNET, G2 og mer! -SECURE YOUR DIGITAL LIFE -Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. +SIKRE DIT DIGITALE LIV +Sikre ditt digitale liv og beskytte mot datainnbrudd ved å generere og lagre unike, sterke passord for hver konto. Oppretthold alt i et ende-til-ende kryptert passordhvelv som bare du har tilgang til. -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +FÅ TILGANG TIL DINE DATA, HVOR SOM HELST, NÅR som helst, PÅ ENHVER ENHET +Administrer, lagre, sikre og del ubegrensede passord på tvers av ubegrensede enheter uten begrensninger. -EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +ALLE BØR HA VERKTØYET FOR Å HOLDE SIKKERHET PÅ PÅ NETT +Bruk Bitwarden gratis uten annonser eller salgsdata. Bitwarden mener alle bør ha muligheten til å være trygge på nettet. Premium-planer gir tilgang til avanserte funksjoner. -EMPOWER YOUR TEAMS WITH BITWARDEN -Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. +STYR LAGEN DINE MED BITWARDEN +Planer for Teams og Enterprise kommer med profesjonelle forretningsfunksjoner. Noen eksempler inkluderer SSO-integrasjon, selvhosting, katalogintegrering og SCIM-klargjøring, globale retningslinjer, API-tilgang, hendelseslogger og mer. -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +Bruk Bitwarden til å sikre arbeidsstyrken din og dele sensitiv informasjon med kolleger. -More reasons to choose Bitwarden: +Flere grunner til å velge Bitwarden: -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Kryptering i verdensklasse +Passord er beskyttet med avansert ende-til-ende-kryptering (AES-256 bit, saltet hashtag og PBKDF2 SHA-256) slik at dataene dine forblir sikre og private. -3rd-party Audits -Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. +Tredjepartsrevisjoner +Bitwarden gjennomfører regelmessig omfattende tredjeparts sikkerhetsrevisjoner med bemerkelsesverdige sikkerhetsfirmaer. Disse årlige revisjonene inkluderer kildekodevurderinger og penetrasjonstesting på tvers av Bitwarden IP-er, servere og webapplikasjoner. -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +Avansert 2FA +Sikre påloggingen din med en tredjeparts autentisering, e-postkoder eller FIDO2 WebAuthn-legitimasjon som en maskinvaresikkerhetsnøkkel eller passord. Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +Overfør data direkte til andre mens du opprettholder ende-til-ende kryptert sikkerhet og begrenser eksponeringen. -Built-in Generator -Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. +Innebygd generator +Lag lange, komplekse og distinkte passord og unike brukernavn for hvert nettsted du besøker. Integrer med e-postaliasleverandører for ekstra personvern. -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +Globale oversettelser +Bitwarden-oversettelser finnes for mer enn 60 språk, oversatt av det globale samfunnet gjennom Crowdin. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +Applikasjoner på tvers av plattformer +Sikre og del sensitive data i Bitwarden Vault fra hvilken som helst nettleser, mobilenhet eller stasjonær OS, og mer. -Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +Bitwarden sikrer mer enn bare passord +End-to-end krypterte legitimasjonsadministrasjonsløsninger fra Bitwarden gir organisasjoner mulighet til å sikre alt, inkludert utviklerhemmeligheter og passordopplevelser. Besøk Bitwarden.com for å lære mer om Bitwarden Secrets Manager og Bitwarden Passwordless.dev! - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Hjemme, på jobben eller på farten sikrer Bitwarden enkelt alle passordene dine, passordene og sensitiv informasjon. Synkroniser og få tilgang til ditt hvelv fra alle dine enheter diff --git a/apps/cli/package.json b/apps/cli/package.json index a7221122225..391c9c80808 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2025.1.1", + "version": "2025.1.3", "keywords": [ "bitwarden", "password", diff --git a/apps/cli/src/platform/services/cli-sdk-load.service.ts b/apps/cli/src/platform/services/cli-sdk-load.service.ts new file mode 100644 index 00000000000..ee3b48e34d7 --- /dev/null +++ b/apps/cli/src/platform/services/cli-sdk-load.service.ts @@ -0,0 +1,9 @@ +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; +import * as sdk from "@bitwarden/sdk-internal"; + +export class CliSdkLoadService implements SdkLoadService { + async load(): Promise { + const module = await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"); + (sdk as any).init(module); + } +} diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 6c715640613..ef07feb9fab 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -68,6 +68,7 @@ import { RegionConfig, } from "@bitwarden/common/platform/abstractions/environment.service"; import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { KeySuffixOptions, LogLevelType } from "@bitwarden/common/platform/enums"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; @@ -171,6 +172,7 @@ import { import { CliBiometricsService } from "../key-management/cli-biometrics-service"; import { flagEnabled } from "../platform/flags"; import { CliPlatformUtilsService } from "../platform/services/cli-platform-utils.service"; +import { CliSdkLoadService } from "../platform/services/cli-sdk-load.service"; import { ConsoleLogService } from "../platform/services/console-log.service"; import { I18nService } from "../platform/services/i18n.service"; import { LowdbStorageService } from "../platform/services/lowdb-storage.service"; @@ -270,6 +272,7 @@ export class ServiceContainer { kdfConfigService: KdfConfigService; taskSchedulerService: TaskSchedulerService; sdkService: SdkService; + sdkLoadService: SdkLoadService; cipherAuthorizationService: CipherAuthorizationService; constructor() { @@ -570,6 +573,7 @@ export class ServiceContainer { const sdkClientFactory = flagEnabled("sdk") ? new DefaultSdkClientFactory() : new NoopSdkClientFactory(); + this.sdkLoadService = new CliSdkLoadService(); this.sdkService = new DefaultSdkService( sdkClientFactory, this.environmentService, @@ -859,6 +863,7 @@ export class ServiceContainer { return; } + await this.sdkLoadService.load(); await this.storageService.init(); await this.stateService.init(); this.containerService.attachToGlobal(global); diff --git a/apps/cli/src/tools/import.command.ts b/apps/cli/src/tools/import.command.ts index f826cb24b7d..d68ef4d043a 100644 --- a/apps/cli/src/tools/import.command.ts +++ b/apps/cli/src/tools/import.command.ts @@ -2,10 +2,14 @@ // @ts-strict-ignore import { OptionValues } from "commander"; import * as inquirer from "inquirer"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + OrganizationService, + getOrganizationById, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { ImportServiceAbstraction, ImportType } from "@bitwarden/importer/core"; @@ -24,16 +28,12 @@ export class ImportCommand { async run(format: ImportType, filepath: string, options: OptionValues): Promise { const organizationId = options.organizationid; if (organizationId != null) { - const userId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); if (!userId) { return Response.badRequest("No user found."); } const organization = await firstValueFrom( - this.organizationService - .organizations$(userId) - .pipe(map((organizations) => organizations.find((o) => o.id === organizationId))), + this.organizationService.organizations$(userId).pipe(getOrganizationById(organizationId)), ); if (organization == null) { diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 4302f302473..2922035a913 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -20,7 +20,7 @@ "**/node_modules/@bitwarden/desktop-napi/index.js", "**/node_modules/@bitwarden/desktop-napi/desktop_napi.${platform}-${arch}*.node" ], - "electronVersion": "33.3.1", + "electronVersion": "34.0.0", "generateUpdatesFilesForAllChannels": true, "publish": { "provider": "generic", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 249145eb3ea..c063f2573d1 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2025.1.4", + "version": "2025.1.8", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 66fed8bcf28..6029e9decc2 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -1,19 +1,21 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; +import { AuthenticationTimeoutComponent } from "@bitwarden/angular/auth/components/authentication-timeout.component"; import { DesktopDefaultOverlayPosition, EnvironmentSelectorComponent, } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { TwoFactorTimeoutComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-expired.component"; import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap"; import { authGuard, lockGuard, + activeAuthGuard, redirectGuard, tdeDecryptionRequiredGuard, unauthGuardFn, } from "@bitwarden/angular/auth/guards"; +import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { twofactorRefactorSwap } from "@bitwarden/angular/utils/two-factor-component-refactor-route-swap"; import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { @@ -37,7 +39,10 @@ import { DevicesIcon, SsoComponent, TwoFactorTimeoutIcon, + NewDeviceVerificationComponent, + DeviceVerificationIcon, } from "@bitwarden/auth/angular"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { LockComponent } from "@bitwarden/key-management/angular"; import { NewDeviceVerificationNoticePageOneComponent, @@ -97,12 +102,12 @@ const routes: Routes = [ }, ), { - path: "2fa-timeout", + path: "authentication-timeout", component: AnonLayoutWrapperComponent, children: [ { path: "", - component: TwoFactorTimeoutComponent, + component: AuthenticationTimeoutComponent, }, ], data: { @@ -112,6 +117,25 @@ const routes: Routes = [ }, } satisfies RouteDataProperties & AnonLayoutWrapperData, }, + { + path: "device-verification", + component: AnonLayoutWrapperComponent, + canActivate: [ + canAccessFeature(FeatureFlag.NewDeviceVerification), + unauthGuardFn(), + activeAuthGuard(), + ], + children: [{ path: "", component: NewDeviceVerificationComponent }], + data: { + pageIcon: DeviceVerificationIcon, + pageTitle: { + key: "verifyIdentity", + }, + pageSubtitle: { + key: "weDontRecognizeThisDevice", + }, + } satisfies RouteDataProperties & AnonLayoutWrapperData, + }, { path: "register", component: RegisterComponent }, { path: "new-device-notice", diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index de80f95593a..72c2821bf33 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -11,6 +11,7 @@ import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/comm import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; @@ -47,11 +48,13 @@ export class InitService { private versionService: VersionService, private sshAgentService: SshAgentService, private autofillService: DesktopAutofillService, + private sdkLoadService: SdkLoadService, @Inject(DOCUMENT) private document: Document, ) {} init() { return async () => { + await this.sdkLoadService.load(); await this.sshAgentService.init(); this.nativeMessagingService.init(); await this.stateService.init({ runMigrations: false }); // Desktop will run them in main process diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 8b890032443..576d36d0104 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -67,6 +67,7 @@ import { import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service"; @@ -77,7 +78,9 @@ import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling"; import { Fido2AuthenticatorService } from "@bitwarden/common/platform/services/fido2/fido2-authenticator.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory"; +import { DefaultSdkLoadService } from "@bitwarden/common/platform/services/sdk/default-sdk-load.service"; import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory"; +import { NoopSdkLoadService } from "@bitwarden/common/platform/services/sdk/noop-sdk-load.service"; import { SystemService } from "@bitwarden/common/platform/services/system.service"; import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage @@ -393,6 +396,11 @@ const safeProviders: SafeProvider[] = [ useClass: flagEnabled("sdk") ? DefaultSdkClientFactory : NoopSdkClientFactory, deps: [], }), + safeProvider({ + provide: SdkLoadService, + useClass: flagEnabled("sdk") ? DefaultSdkLoadService : NoopSdkLoadService, + deps: [], + }), safeProvider({ provide: LoginEmailService, useClass: LoginEmailService, diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index 1710005ab66..6a5764e4c9f 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -885,6 +885,15 @@ "message": "Bevestig met Duo Security vir u organisasie d.m.v. die Duo Mobile-toep, SMS, spraakoproep of ’n U2F-beveiligingsleutel.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index c2ccab5b908..ecbacf95a8d 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -885,6 +885,15 @@ "message": "تحقق من خلال نظام الحماية الثنائي لمؤسستك باستخدام تطبيق Duo Mobile أو الرسائل القصيرة أو المكالمة الهاتفية أو مفتاح الأمان U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 68753f204ea..dfd7942e033 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -885,6 +885,15 @@ "message": "Təşkilatınızını Duo Security ilə doğrulamaq üçün Duo Mobile tətbiqi, SMS, telefon zəngi və ya U2F güvənlik açarını istifadə edin.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Kimliyinizi doğrulayın" + }, + "weDontRecognizeThisDevice": { + "message": "Bu cihazı tanımırıq. Kimliyinizi doğrulamaq üçün e-poçtunuza göndərilən kodu daxil edin." + }, + "continueLoggingIn": { + "message": "Giriş etməyə davam" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "Bu özəllik, ödənişsiz təşkilatlar üçün əlçatan deyil. Daha çox özəlliyin kilidini açmaq üçün ödənişli plana keçin." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Uzantını güncəlləmək tələb olunur" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "İstifadə etdiyiniz brauzer uzantısı köhnəlib. Lütfən onu güncəlləyin, ya da masaüstü tətbiq ayarlarında brauzer inteqrasiyası üzrə barmaq izi ilə doğrulamanı sıradan çıxardın." } } diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index 70b30128f9b..7b8cc90d072 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -885,6 +885,15 @@ "message": "Праверка з дапамогай Duo Security для вашай арганізацыі, выкарыстоўваючы праграму Duo Mobile, SMS, тэлефонны выклік або ключ бяспекі U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index b3052fbc6cf..48dc71a16cd 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -885,6 +885,15 @@ "message": "Удостоверяване чрез Duo Security за организацията ви, с ползване на приложението Duo Mobile, SMS, телефонен разговор или устройство U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Потвърдете самоличността си" + }, + "weDontRecognizeThisDevice": { + "message": "Това устройство е непознато. Въведете кода изпратен на е-пощата Ви, за да потвърдите самоличността си." + }, + "continueLoggingIn": { + "message": "Продължаване с вписването" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "Безплатните планове нямат достъп до тази функционалност. Преминете към платен план, за да се възползвате от тази и много други възможности." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Необходимо е обновяване на добавката" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "Добавката за браузъра, която използвате, е остаряла. Моля, обновете я или изключете интеграцията за проверка на пръстов отпечатък в браузъра от настройките на самостоятелното приложение." } } diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 7e1994cdeab..e559bf83b22 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -885,6 +885,15 @@ "message": "Duo Mobile app, এসএমএস, ফোন কল, বা U2F সুরক্ষা কী ব্যবহার করে আপনার সংস্থার জন্য Duo Security এর মাধ্যমে যাচাই করুন।", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 487d98a327f..b4c616cdd01 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -885,6 +885,15 @@ "message": "Potvrdi sa Duo Security za svoju organizaciju pomoću aplikacije Duo Mobile, SMS-om, telefonskim pozivom ili U2F sigurnosnim ključem.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 8b4ee8e276d..8f8ee393b69 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -885,6 +885,15 @@ "message": "Verifiqueu amb Duo Security per a la vostra organització mitjançant l'aplicació Duo Mobile, SMS, trucada telefònica o clau de seguretat U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 28c20ee5184..a4f1f50ce93 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -885,6 +885,15 @@ "message": "Ověření pomocí Duo Security pro Vaši organizaci prostřednictvím aplikace Duo Mobile, SMS, telefonního hovoru nebo bezpečnostního klíče U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Ověřte svou totožnost" + }, + "weDontRecognizeThisDevice": { + "message": "Toto zařízení nepoznáváme. Zadejte kód zaslaný na Váš e-mail pro ověření Vaší totožnosti." + }, + "continueLoggingIn": { + "message": "Pokračovat v přihlášení" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "Tato funkce není dostupná pro bezplatné organizace. Přepněte na placenou verzi a odemkněte další funkce." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Je vyžadována aktualiazce rozšíření" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "Použité rozšíření prohlížeče je zastaralé. Aktualizujte jej nebo zakažte ověření otisků prstů při integraci prohlížeče v nastavení aplikace." } } diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 897b5bb5508..22e19c84310 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -885,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index 0d1c94527b2..5545e856b64 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -885,6 +885,15 @@ "message": "Bekræft med Duo Security for din organisation vha. Duo Mobile-app, SMS, telefonopkald eller U2F-sikkerhedsnøgle.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Bekræft identiteten" + }, + "weDontRecognizeThisDevice": { + "message": "Denne enhed er ikke genkendt. Angiv koden i den tilsendte e-mail for at bekræfte identiteten." + }, + "continueLoggingIn": { + "message": "Fortsæt med at logge ind" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "Denne funktion er utilgængelig for gratis organisationer. Skift til en betalingsabonnementstype for at oplåse flere funktioner." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Udvidelsesopdatering krævet" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "Den anvendte webbrowserudvidelse er forældet. Opdatér den venligst eller deaktivér webbrowserintegreret fingeraftryksbekræftelse i computer-app indstillingerne." } } diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 5ce63034cb9..4f7740f46c8 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -256,7 +256,7 @@ "message": "Bitwarden konnte folgende(n) Tresor-Eintrag/Einträge nicht entschlüsseln." }, "contactCSToAvoidDataLossPart1": { - "message": "Kundensupport kontaktieren", + "message": "Kontaktiere unser Customer Success Team", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { @@ -885,6 +885,15 @@ "message": "Nutze Duo Security, um dich mit der Duo-Mobile-App, SMS, per Anruf oder U2F-Sicherheitsschlüssel bei deiner Organisation zu verifizieren.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verifiziere deine Identität" + }, + "weDontRecognizeThisDevice": { + "message": "Wir erkennen dieses Gerät nicht. Gib den an deine E-Mail-Adresse gesendeten Code ein, um deine Identität zu verifizieren." + }, + "continueLoggingIn": { + "message": "Anmeldung fortsetzen" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "Diese Funktion ist für kostenlose Organisationen nicht verfügbar. Wechsle zu einem kostenpflichtigen Abonnement, um weitere Funktionen freizuschalten." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Aktualisierung der Erweiterung notwendig" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "Die von dir verwendete Browser-Erweiterung ist veraltet. Bitte aktualisiere sie oder deaktiviere die Fingerabdrucküberprüfung der Browser-Integration in den Einstellungen der Desktop-App." } } diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 3fed18252fe..8f62ceda6ca 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -27,7 +27,7 @@ "message": "Ασφαλής σημείωση" }, "typeSshKey": { - "message": "SSH key" + "message": "Κλειδί SSH" }, "folders": { "message": "Φάκελοι" @@ -64,7 +64,7 @@ } }, "welcomeBack": { - "message": "Welcome back" + "message": "Καλωσορίσατε και πάλι" }, "moveToOrgDesc": { "message": "Επιλέξτε έναν οργανισμό στον οποίο θέλετε να μετακινήσετε αυτό το στοιχείο. Η μετακίνηση σε έναν οργανισμό μεταβιβάζει την ιδιοκτησία του στοιχείου σε αυτό τον οργανισμό. Δεν θα είστε πλέον ο άμεσος ιδιοκτήτης αυτού του στοιχείου μόλις το μετακινήσετε." @@ -107,7 +107,7 @@ "message": "Επεξεργασία στοιχείου" }, "emailAddress": { - "message": "Διεύθυνση ηλ. ταχυδρομείου" + "message": "Διεύθυνση Email" }, "verificationCodeTotp": { "message": "Κωδικός Επαλήθευσης (TOTP)" @@ -181,16 +181,16 @@ "message": "Διεύθυνση" }, "sshPrivateKey": { - "message": "Private key" + "message": "Ιδιωτικό κλειδί" }, "sshPublicKey": { - "message": "Public key" + "message": "Δημόσιο κλειδί" }, "sshFingerprint": { - "message": "Fingerprint" + "message": "Αποτύπωμα" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "Τύπος κλειδιού" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -205,37 +205,37 @@ "message": "RSA 4096-Bit" }, "sshKeyGenerated": { - "message": "A new SSH key was generated" + "message": "Δημιουργήθηκε ένα νέο SSH κλειδί" }, "sshKeyWrongPassword": { - "message": "The password you entered is incorrect." + "message": "Ο κωδικός πρόσβασης που εισάγατε είναι λάθος." }, "importSshKey": { - "message": "Import" + "message": "Εισαγωγή" }, "confirmSshKeyPassword": { - "message": "Confirm password" + "message": "Επιβεβαίωση κωδικού πρόσβασης" }, "enterSshKeyPasswordDesc": { - "message": "Enter the password for the SSH key." + "message": "Εισάγετε τον κωδικό πρόσβασης για το SSH κλειδί." }, "enterSshKeyPassword": { - "message": "Enter password" + "message": "Εισάγετε κωδικό πρόσβασης" }, "sshAgentUnlockRequired": { - "message": "Please unlock your vault to approve the SSH key request." + "message": "Παρακαλώ ξεκλειδώστε την κρύπτη σας για να εγκρίνετε το αίτημα κλειδιού SSH." }, "sshAgentUnlockTimeout": { - "message": "SSH key request timed out." + "message": "Λήξη χρονικού ορίου αιτήματος κλειδιού SSH." }, "enableSshAgent": { - "message": "Enable SSH agent" + "message": "Ενεργοποίηση πράκτορα SSH" }, "enableSshAgentDesc": { - "message": "Enable the SSH agent to sign SSH requests right from your Bitwarden vault." + "message": "Ενεργοποιήστε τον πράκτορα SSH για την υπογραφή αιτημάτων SSH απευθείας από την κρύπτη Bitwarden." }, "enableSshAgentHelp": { - "message": "The SSH agent is a service targeted at developers that allows you to sign SSH requests directly from your Bitwarden vault." + "message": "Ο πράκτορα SSH είναι μια υπηρεσία που απευθύνεται σε προγραμματιστές που σας επιτρέπει να υπογράψετε αιτήματα SSH απευθείας από την κρύπτη Bitwarden σας." }, "premiumRequired": { "message": "Απαιτείται Premium" @@ -250,17 +250,17 @@ "message": "Σφάλμα" }, "decryptionError": { - "message": "Decryption error" + "message": "Σφάλμα αποκρυπτογράφησης" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Το Bitwarden δεν μπόρεσε να αποκρυπτογραφήσει τα αντικείμενα κρύπτης που αναφέρονται παρακάτω." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "Εποικινωνία επιτυχίας επαφής πελάτη", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "για την αποφυγή επιπλέον απώλειας δεδομένων.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "january": { @@ -475,10 +475,10 @@ "message": "Αντιγραφή κωδικού πρόσβασης" }, "regenerateSshKey": { - "message": "Regenerate SSH key" + "message": "Επαναδημιουργία κλειδιού SSH" }, "copySshPrivateKey": { - "message": "Copy SSH private key" + "message": "Αντιγραφή ιδιωτικού κλειδιού SSH" }, "copyPassphrase": { "message": "Αντιγραφή φράσης πρόσβασης", @@ -638,7 +638,7 @@ "message": "Δημιουργία λογαριασμού" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Μόλις ήρθες στο Bitwarden;" }, "setAStrongPassword": { "message": "Ορίστε έναν ισχυρό κωδικό πρόσβασης" @@ -650,16 +650,16 @@ "message": "Είσοδος" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Σύνδεση στο Bitwarden" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Σύνδεση με κλειδί πρόσβασης" }, "loginWithDevice": { - "message": "Log in with device" + "message": "Σύνδεση με χρήση συσκευής" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "Χρήση single sign-on" }, "submit": { "message": "Υποβολή" @@ -885,6 +885,15 @@ "message": "Επαληθεύστε με το Duo Security για τον οργανισμό σας χρησιμοποιώντας την εφαρμογή Duo Mobile, μήνυμα SMS, τηλεφωνική κλήση ή κλειδί ασφαλείας U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -934,10 +943,10 @@ "message": "URL Διακομιστή" }, "authenticationTimeout": { - "message": "Authentication timeout" + "message": "Χρονικό όριο επαλήθευσης" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "Λήξη χρονικού ορίου συνεδρίας επαλήθευσης. Παρακαλώ επανεκκινήστε τη διαδικασία σύνδεσης." }, "selfHostBaseUrl": { "message": "URL διακομιστή αυτο-φιλοξενίας", @@ -1407,13 +1416,13 @@ "message": "Ιστορικό κωδικού πρόσβασης" }, "generatorHistory": { - "message": "Generator history" + "message": "Ιστορικό γεννήτριας" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "Εκκαθάριση ιστορικού γεννήτριας" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "Αν συνεχίσετε, όλες οι καταχωρήσεις θα διαγραφούν οριστικά από το ιστορικό της γεννήτριας. Σίγουρα θέλετε να συνεχίσετε;" }, "clear": { "message": "Εκκαθάριση", @@ -1423,13 +1432,13 @@ "message": "Δεν υπάρχουν κωδικοί στη λίστα." }, "clearHistory": { - "message": "Clear history" + "message": "Διαγραφή ιστορικού" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Τίποτα για προβολή" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "Δεν έχετε δημιουργήσει τίποτα πρόσφατα" }, "undo": { "message": "Αναίρεση" @@ -1785,10 +1794,10 @@ "message": "Η διαγραφή του λογαριασμού σας είναι μόνιμη. Δεν μπορεί να αναιρεθεί." }, "cannotDeleteAccount": { - "message": "Cannot delete account" + "message": "Αδυναμία διαγραφής λογαριασμού" }, "cannotDeleteAccountDesc": { - "message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details." + "message": "Αυτή η ενέργεια δεν μπορεί να ολοκληρωθεί επειδή ο λογαριασμός σας ανήκει σε έναν οργανισμό. Επικοινωνήστε με το διαχειριστή του οργανισμού σας για πρόσθετες λεπτομέρειες." }, "accountDeleted": { "message": "Ο λογαριασμός διαγράφηκε" @@ -2495,7 +2504,7 @@ "message": "Δημιουργία διεύθυνσης ηλ. ταχυδρομείου" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "Η τιμή πρέπει να είναι μεταξύ $MIN$ και $MAX$.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -2509,7 +2518,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": " Χρησιμοποιήστε $RECOMMENDED$ ή περισσότερους χαρακτήρες για να δημιουργήσετε έναν ισχυρό κωδικό πρόσβασης.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2519,7 +2528,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", + "message": " Χρησιμοποιήστε $RECOMMENDED$ ή περισσότερες λέξεις για να δημιουργήσετε μια ισχυρή φράση πρόσβασης.", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2740,13 +2749,13 @@ "message": "Μια ειδοποίηση έχει σταλεί στη συσκευή σας." }, "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "message": "Μια ειδοποίηση στάλθηκε στη συσκευή σας" }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + "message": "Βεβαιωθείτε ότι ο λογαριασμός σας είναι ξεκλείδωτος και ότι η φράση δακτυλικού αποτυπώματος ταιριάζει στην άλλη συσκευή" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "Χρειάζεστε μια άλλη επιλογή;" }, "fingerprintMatchInfo": { "message": "Παρακαλώ βεβαιωθείτε ότι το θησαυ/κιό σας είναι ξεκλείδωτο και η φράση δακτυλικών αποτυπωμάτων ταιριάζει με την άλλη συσκευή." @@ -2755,13 +2764,13 @@ "message": "Φράση δακτυλικών αποτυπωμάτων" }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "Θα ειδοποιηθείτε μόλις εγκριθεί η αίτηση" }, "needAnotherOption": { "message": "Η σύνδεση με τη χρήση συσκευής πρέπει να οριστεί στις ρυθμίσεις της εφαρμογής Bitwarden. Χρειάζεστε κάποια άλλη επιλογή;" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "Δείτε όλες τις επιλογές σύνδεσης" }, "viewAllLoginOptions": { "message": "Δείτε όλες τις επιλογές σύνδεσης" @@ -2880,16 +2889,16 @@ "message": "Βρέθηκε και ταυτοποιήθηκε αδύναμος κωδικός σε μια διαρροή δεδομένων. Χρησιμοποιήστε ένα ισχυρό και μοναδικό κωδικό πρόσβασης για την προστασία του λογαριασμού σας. Είστε σίγουροι ότι θέλετε να χρησιμοποιήσετε αυτόν τον κωδικό πρόσβασης;" }, "useThisPassword": { - "message": "Use this password" + "message": "Χρήση αυτού του κωδικού πρόσβασης" }, "useThisUsername": { - "message": "Use this username" + "message": "Χρήση αυτού του ονόματος χρήστη" }, "checkForBreaches": { "message": "Ελέγξτε γνωστές διαρροές δεδομένων για αυτόν τον κωδικό" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "Έχετε συνδεθεί!" }, "important": { "message": "Σημαντικό:" @@ -2922,16 +2931,16 @@ "message": "Ενημέρωση Προτεινόμενων Ρυθμίσεων" }, "rememberThisDeviceToMakeFutureLoginsSeamless": { - "message": "Remember this device to make future logins seamless" + "message": "Απομνημόνευση αυτής της συσκευής για απρόσκοπτη σύνδεση στο μέλλον" }, "deviceApprovalRequired": { "message": "Απαιτείται έγκριση συσκευής. Επιλέξτε μια επιλογή έγκρισης παρακάτω:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "Απαιτείται έγκριση συσκευής" }, "selectAnApprovalOptionBelow": { - "message": "Select an approval option below" + "message": "Επιλέξτε μια επιλογή έγκρισης παρακάτω" }, "rememberThisDevice": { "message": "Απομνημόνευση αυτής της συσκευής" @@ -2986,7 +2995,7 @@ "message": "Η διεύθυνση ηλ. ταχυδρομείου του χρήστη λείπει" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "Το email χρήστη δεν βρέθηκε. Αποσυνδεθήκατε." }, "deviceTrusted": { "message": "Αξιόπιστη συσκευή" @@ -3383,19 +3392,19 @@ "message": "Δεν βρέθηκαν ελεύθερες θύρες για τη σύνδεση sso." }, "biometricsStatusHelptextUnlockNeeded": { - "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο επειδή απαιτείται πρώτα το ξεκλείδωμα με PIN ή κωδικό πρόσβασης." }, "biometricsStatusHelptextHardwareUnavailable": { - "message": "Biometric unlock is currently unavailable." + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο προς το παρόν." }, "biometricsStatusHelptextAutoSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο λόγω εσφαλμένων ρυθμίσεων αρχείων συστήματος." }, "biometricsStatusHelptextManualSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο λόγω εσφαλμένων ρυθμίσεων αρχείων συστήματος." }, "biometricsStatusHelptextNotEnabledLocally": { - "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο επειδή δεν είναι ενεργοποιημένο για το $EMAIL$ στην εφαρμογή Bitwarden για υπολογιστές.", "placeholders": { "email": { "content": "$1", @@ -3404,58 +3413,58 @@ } }, "biometricsStatusHelptextUnavailableReasonUnknown": { - "message": "Biometric unlock is currently unavailable for an unknown reason." + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο για άγνωστο λόγο." }, "authorize": { - "message": "Authorize" + "message": "Εξουσιοδότηση" }, "deny": { - "message": "Deny" + "message": "Απόρριψη" }, "sshkeyApprovalTitle": { - "message": "Confirm SSH key usage" + "message": "Επιβεβαίωση χρήσης κλειδιού SSH" }, "sshkeyApprovalMessageInfix": { - "message": "is requesting access to" + "message": "ζητά πρόσβαση σε" }, "unknownApplication": { - "message": "An application" + "message": "Μια εφαρμογή" }, "sshKeyPasswordUnsupported": { - "message": "Importing password protected SSH keys is not yet supported" + "message": "Η εισαγωγή κλειδιών SSH που προστατεύονται με κωδικό πρόσβασης δεν υποστηρίζεται ακόμη" }, "invalidSshKey": { - "message": "The SSH key is invalid" + "message": "Το SSH κλειδί δεν είναι έγκυρο" }, "sshKeyTypeUnsupported": { - "message": "The SSH key type is not supported" + "message": "Ο τύπος κλειδιού SSH δεν υποστηρίζεται" }, "importSshKeyFromClipboard": { - "message": "Import key from clipboard" + "message": "Εισαγωγή κλειδιού από το πρόχειρο" }, "sshKeyPasted": { - "message": "SSH key imported successfully" + "message": "Το SSH κλειδί εισήχθη με επιτυχία" }, "fileSavedToDevice": { "message": "Το αρχείο αποθηκεύτηκε στη συσκευή. Διαχείριση από τις λήψεις της συσκευής σας." }, "importantNotice": { - "message": "Important notice" + "message": "Σημαντική ειδοποίηση" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "Ρύθμιση σύνδεσης δύο βημάτων" }, "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + "message": "Το Bitwarden θα στείλει έναν κωδικό στο email του λογαριασμού σας για να επαληθεύσει τις συνδέσεις από νέες συσκευές ξεκινώντας από τον Φεβρουάριο του 2025." }, "newDeviceVerificationNoticeContentPage2": { - "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + "message": "Μπορείτε να ορίσετε σύνδεση δύο βημάτων ως εναλλακτικό τρόπο προστασίας του λογαριασμού σας ή να αλλάξετε το email σας σε ένα που μπορείτε να έχετε πρόσβαση." }, "remindMeLater": { - "message": "Remind me later" + "message": "Υπενθύμιση αργότερα" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "Έχετε αξιόπιστη πρόσβαση στο email σας, $EMAIL$;", "placeholders": { "email": { "content": "$1", @@ -3464,24 +3473,30 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "Όχι, δεν έχω" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "Ναι, μπορώ να συνδεθώ αξιόπιστα στο ηλ. ταχυδρομείο email μου" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "Ενεργοποίηση σύνδεσης δύο βημάτων" }, "changeAcctEmail": { - "message": "Change account email" + "message": "Αλλαγή email λογαριασμού" }, "organizationUpgradeRequired": { - "message": "Organization upgrade required" + "message": "Απαιτείται αναβάθμιση οργανισμού" }, "upgradeOrganization": { - "message": "Upgrade organization" + "message": "Αναβάθμιση οργανισμού" }, "upgradeOrganizationDesc": { - "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + "message": "Αυτή η λειτουργία, δεν είναι διαθέσιμη στους δωρεάν οργανισμούς. Μεταβείτε σε ένα πακέτο επί πληρωμής για να ξεκλειδώσετε περισσότερες λειτουργίες." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Απαιτείται ενημέρωση επέκτασης" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "Η επέκταση περιηγητή που χρησιμοποιείτε είναι ξεπερασμένη. Παρακαλούμε ενημερώστε την ή απενεργοποιήστε την επικύρωση δακτυλικών αποτυπωμάτων του προγράμματος περιήγησης στις ρυθμίσεις της εφαρμογής για την επιφάνεια εργασίας." } } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 8ae2cdc3ef6..4f18a88d994 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -885,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index b33f2644403..986a6ad2e3a 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -885,6 +885,15 @@ "message": "Verify with Duo Security for your organisation using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognise this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organisations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index e3780c16984..9afd76f697a 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -885,6 +885,15 @@ "message": "Verify with Duo Security for your organisation using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognise this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organisations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index 4ba31aa743b..ce5be3b8c63 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -885,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index 00e17cadc40..df6dded0477 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -885,6 +885,15 @@ "message": "Verificar con Duo Security para tu organización usando la aplicación Duo Mobile, SMS, llamada telefónica o llave de seguridad U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index a34820dc5a8..e7c33c432ba 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -885,6 +885,15 @@ "message": "Kinnita organisatsiooni jaoks Duo Security abil, kasutades selleks Duo Mobile rakendust, SMS-i, telefonikõnet või U2F turvavõtit.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index ff7faec50b1..d0e62383468 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -885,6 +885,15 @@ "message": "Egiaztatu zure erakunderako Duo Securityrekin Duo Mobile aplikazioa, SMS, telefono deia edo U2F segurtasun-gakoa erabiliz.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index 710e7ec3460..ea25d5057e9 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -885,6 +885,15 @@ "message": "از Duo Security با استفاده از برنامه تلفن همراه، پیامک، تماس تلفنی یا کلید امنیتی U2F برای تأیید سازمان خود استفاده کنید.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index fd3d9a8a3df..d05cf0e156c 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -250,10 +250,10 @@ "message": "Virhe" }, "decryptionError": { - "message": "Decryption error" + "message": "Salauksen purkuvirhe" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden ei voinut purkaa alla olevia holvin kohteita." }, "contactCSToAvoidDataLossPart1": { "message": "Contact customer success", @@ -885,6 +885,15 @@ "message": "Vahvista organisaatiollesi Duo Securityn avulla käyttäen Duo Mobile ‑sovellusta, tekstiviestiä, puhelua tai U2F-suojausavainta.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2880,10 +2889,10 @@ "message": "Havaittiin heikko ja tietovuodosta löytynyt salasana. Sinun tulisi suojata tilisi vahvalla ja ainutlaatuisella salasanalla. Haluatko varmasti käyttää tätä salasanaa?" }, "useThisPassword": { - "message": "Use this password" + "message": "Käytä tätä salasanaa" }, "useThisUsername": { - "message": "Use this username" + "message": "Käytä tätä käyttäjätunnusta" }, "checkForBreaches": { "message": "Tarkasta esiintyykö salasanaa tunnetuissa tietovuodoissa" @@ -3386,7 +3395,7 @@ "message": "Biometric unlock is unavailable because PIN or password unlock is required first." }, "biometricsStatusHelptextHardwareUnavailable": { - "message": "Biometric unlock is currently unavailable." + "message": "Biometrinen lukituksen avaus ei ole tällä hetkellä käytettävissä." }, "biometricsStatusHelptextAutoSetupNeeded": { "message": "Biometric unlock is unavailable due to misconfigured system files." @@ -3476,12 +3485,18 @@ "message": "Muuta tilin sähköpostiosoitetta" }, "organizationUpgradeRequired": { - "message": "Organization upgrade required" + "message": "Organisaation päivitys vaaditaan" }, "upgradeOrganization": { - "message": "Upgrade organization" + "message": "Päivitä organisaatio" }, "upgradeOrganizationDesc": { - "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + "message": "Ominaisuus ei ole ilmaisorganisaatioiden käytettävissä. Avaa lisää ominaisuuksia vaihtamalla maksulliseen tilaukseen." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 179cc7a2a55..684c57b3c84 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -885,6 +885,15 @@ "message": "Patunayan sa Duo Security para sa iyong samahan gamit ang Duo Mobile app, SMS, tawag sa telepono, o U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 5e6c9ed71a0..476799e97d1 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -885,6 +885,15 @@ "message": "Sécurisez votre organisation avec Duo Security à l'aide de l'application Duo Mobile, l'envoi d'un SMS, un appel vocal ou une clé de sécurité U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "WebAuthn FIDO2" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index bca12f16a7d..4f18a88d994 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -885,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 853e950dc0e..a8c34bd61e7 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -885,6 +885,15 @@ "message": "בצע אימות מול Duo Security עבור הארגון שלך באמצעות אפליקצית Duo לפלאפון, SMS, שיחת טלפון, או מפתח אבטחה U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 59c0f3df59f..4a608e89d54 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -885,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 5fd7fae86a8..2eceeddde07 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -885,6 +885,15 @@ "message": "Potvrdi s Duo Security za svoju organizaciju pomoću aplikacije Duo Mobile, SMS-om, telefonskim pozivom ili U2F sigurnosnim ključem.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 19fd8a7fe49..809f56c4892 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -885,6 +885,15 @@ "message": "Ellenőrzés szervezeti Duo Security segítségével a Duo Mobile alkalmazás, SMS, telefonhívás vagy U2F biztonsági kulcs használatával.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Személyazonosság ellenőrzése" + }, + "weDontRecognizeThisDevice": { + "message": "Nem ismerhető fel ez az eszköz. Írjuk be az email címünkre küldött kódot a személyazonosság igazolásához." + }, + "continueLoggingIn": { + "message": "A bejelentkezés folytatása" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "Ez a szolgáltatás nem elérhető ingyenes szervezeteknek. Váltás fizetős díjcsomagra a további funkciók feloldásához." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Kiterjesztés frissítés szükséges" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "Al használt böngésző bővítmény elavult. Frissítsük vagy tiltsuk le a böngésző integráció ujjlenyomat ellenőrzését az asztali alkalmazás beállításainál." } } diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 328200858e9..4e5abd62f75 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -885,6 +885,15 @@ "message": "Verifikasi dengan Duo Security untuk organisasi anda dengan menggunakan Aplikasi Duo Mobile, SMS, panggilan telepon atau kunci keamanan U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index e58d2fd665d..ee7da5c3582 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -885,6 +885,15 @@ "message": "Verifica con Duo Security per la tua organizzazione usando l'app Duo Mobile, SMS, chiamata telefonica, o chiave di sicurezza U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verifica la tua identità" + }, + "weDontRecognizeThisDevice": { + "message": "Non riconosciamo questo dispositivo. Inserisci il codice inviato alla tua e-mail per verificare la tua identità." + }, + "continueLoggingIn": { + "message": "Continua l'accesso" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "Questa funzione non è disponibile per le organizzazioni gratuite. Passa a un piano a pagamento per sbloccare più funzioni." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Aggiornamento estensione richiesto" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "L'estensione del browser che stai usando non è aggiornata. Aggiornala o disabilita la convalida dell'impronta digitale per l'integrazione del browser nelle impostazioni dell'app desktop." } } diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index 671ab6c830e..6f6dfaee2f6 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -885,6 +885,15 @@ "message": "組織の Duo Security を Duo Mobile アプリや SMS、電話、U2F セキュリティーキーを使用して認証します。", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index a1951c542ad..e7bb32055b7 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -885,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index bca12f16a7d..4f18a88d994 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -885,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 840b73558c6..23f4cd3464e 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -885,6 +885,15 @@ "message": "ಡ್ಯುಯೊ ಮೊಬೈಲ್ ಅಪ್ಲಿಕೇಶನ್, ಎಸ್‌ಎಂಎಸ್, ಫೋನ್ ಕರೆ ಅಥವಾ ಯು 2 ಎಫ್ ಭದ್ರತಾ ಕೀಲಿಯನ್ನು ಬಳಸಿಕೊಂಡು ನಿಮ್ಮ ಸಂಸ್ಥೆಗಾಗಿ ಡ್ಯುಯೊ ಸೆಕ್ಯುರಿಟಿಯೊಂದಿಗೆ ಪರಿಶೀಲಿಸಿ.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 092cdf08afc..82b41979c09 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -885,6 +885,15 @@ "message": "Duo Mobile 앱, SMS, 전화 통화를 사용한 조직용 Duo Security 또는 U2F 보안 키를 사용하여 인증하세요.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index 1f010d33300..708393624fc 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -885,6 +885,15 @@ "message": "Patikrinkite su Duo Security savo organizacijai naudodami Duo Mobile programą, SMS žinutę, telefono skambutį arba U2F saugumo raktą.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 292cff77fba..e26669f30ab 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -256,7 +256,7 @@ "message": "Bitwarden nevarēja atšifrēt zemāk uzskaitītos glabātavas vienumus." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "Sazināties ar klientu atbalstu", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { @@ -885,6 +885,15 @@ "message": "Apliecināšana ar savas apvienības Duo Security, izmantojot Duo Mobile lietotni, īsziņu, tālruņa zvanu vai U2F drošības atslēgu.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Apliecināt savu identitāti" + }, + "weDontRecognizeThisDevice": { + "message": "Mēs neatpazīstam šo ierīci. Jāievada kods, kas tika nosūtīts e-pastā, lai apliecinātu savu identitāti." + }, + "continueLoggingIn": { + "message": "Turpināt pieteikšanos" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "Šī iespēja nav pieejama bezmaksas apvienībām. Maksas plāna izvēle sniedz plašākas iespējas." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Nepieciešama paplašinājuma atjaunināšana" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "Izmantotais pārlūka paplašinājums ir novecojis. Lūgums atjaunināt to vai atspējot pārlūka sasaistīšanas pirkstu nospieduma pārbaudi darbvirsmas lietotnes iestatījumos." } } diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index a03a80d01cd..46d8cfe8cb7 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -885,6 +885,15 @@ "message": "Potvrdite sa Duo Security za svoju organizaciju pomoću aplikacije Duo Mobile, SMS-a, telefonskog poziva ili U2F sigurnosnog ključa.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 41e4225c2df..1d27bc2182c 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -885,6 +885,15 @@ "message": "Duo Mobile, SMS, ഫോൺ കോൾ അല്ലെങ്കിൽ U2F സുരക്ഷാ കീ ഉപയോഗിച്ച് നിങ്ങളുടെ ഓർഗനൈസേഷനെ Duo Security ഉപയോഗിച്ച് പരിശോധിക്കുക.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index bca12f16a7d..4f18a88d994 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -885,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index 2c16b832f33..46bfc5b7ee3 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -885,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index f6fe08a8979..8c181361b45 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -885,6 +885,15 @@ "message": "Verifiser med Duo Security for din organisasjon gjennom Duo Mobile-appen, SMS, telefonsamtale, eller en U2F-sikkerhetsnøkkel.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 7be28d2ced5..5aefc2febf4 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -885,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index 877acb0d0dc..e0f5a27a5c6 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -885,6 +885,15 @@ "message": "Verificatie met Duo Security middels de Duo Mobile-app, sms, spraakoproep of een U2F-beveiligingssleutel.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Controleer je identiteit" + }, + "weDontRecognizeThisDevice": { + "message": "We herkennen dit apparaat niet. Voer de code in die naar je e-mail is verzonden om je identiteit te verifiëren." + }, + "continueLoggingIn": { + "message": "Doorgaan met inloggen" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "Deze mogelijkheid is niet beschikbaar voor gratis organisaties. Schakel over naar een betaald abonnement om meer mogelijkheden te ontgrendelen." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extensie-uupdate vereist" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "De browserextensie die je gebruikt is verouderd. Werk deze bij of schakel de vingerafdruk validatie van de browserintegratie uit in instellingen van de desktop-app." } } diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index ef5a0603761..74eabeefa2e 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -885,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index 374cbf1bc95..82404647e27 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -885,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index 9cc63555545..c40ef452d49 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -885,6 +885,15 @@ "message": "Weryfikacja dostępu do Twojej organizacji z użyciem Duo Security poprzez aplikację Duo Mobile, SMS, połączenie telefoniczne lub klucz bezpieczeństwa U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index 33bceb61667..71818b81b4b 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -885,6 +885,15 @@ "message": "Verifique com o Duo Security utilizando o aplicativo Duo Mobile, SMS, chamada telefônica, ou chave de segurança U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "WebAuthn FIDO2" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 61aa40e3307..86d8d33144f 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -885,6 +885,15 @@ "message": "Proteja a sua organização com a Duo Security utilizando a aplicação Duo Mobile, SMS, chamada telefónica ou uma chave de segurança U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verifique a sua identidade" + }, + "weDontRecognizeThisDevice": { + "message": "Não reconhecemos este dispositivo. Introduza o código enviado para o seu e-mail para verificar a sua identidade." + }, + "continueLoggingIn": { + "message": "Continuar a iniciar sessão" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "Esta funcionalidade não está disponível para organizações gratuitas. Mude para um plano pago para desbloquear mais funcionalidades." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Atualização da extensão necessária" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "A extensão do navegador que está a utilizar está desatualizada. Atualize-a ou desative a validação de impressões digitais da integração do navegador nas definições da aplicação para computador." } } diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 17b9d050f03..a9d3f1d6109 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -885,6 +885,15 @@ "message": "Verificați cu Duo Security pentru organizația dvs. utilizând aplicația Duo Mobile, SMS, apel telefonic sau cheia de securitate U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 408bc373178..d8b741d4cd4 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -885,6 +885,15 @@ "message": "Подтвердите с помощью Duo Security для вашей организации, используя приложение Duo Mobile, SMS, телефонный звонок или ключ безопасности U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Подтвердите вашу личность" + }, + "weDontRecognizeThisDevice": { + "message": "Мы не распознали это устройство. Введите код, отправленный на ваш email, чтобы подтвердить вашу личность." + }, + "continueLoggingIn": { + "message": "Продолжить вход" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "Эта функция недоступна для бесплатных организаций. Переключитесь на платный план, чтобы разблокировать дополнительные возможности." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Необходимо обновить расширение" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "Используемое вами расширение браузера устарело. Пожалуйста, обновите его или отключите проверку интеграции браузера с помощью отпечатка пальца в настройках приложения для компьютера." } } diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index caec95368e5..eca0b7b9eb7 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -885,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 1c253151ab1..d02bd35546d 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -885,6 +885,15 @@ "message": "Overiť sa prostredníctvom Duo Security vašej organizácie použitím Duo Mobile aplikácie, SMS, telefonátu alebo U2F bezpečnostným kľúčom.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Overte svoju totožnosť" + }, + "weDontRecognizeThisDevice": { + "message": "Toto zariadenie nepoznáme. Na overenie vašej totožnosti zadajte kód, ktorý bol zaslaný na váš e-mail." + }, + "continueLoggingIn": { + "message": "Pokračovať v prihlasovaní" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "Táto funkcia nie je k dispozícii pre bezplatné organizácie. Ak chcete odomknúť ďalšie funkcie, prejdite na platený plán." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Rozšírenie musíte aktualizovať" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "Rozšírenie prehliadača, ktoré používate, je zastarané. Aktualizujte ho alebo zakážte overenie odtlačkov integrácie prehliadača v nastaveniach desktopovej aplikácie." } } diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index cd36695335b..d5c26aa266f 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -885,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index d4109a70bf5..a647521a096 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -885,6 +885,15 @@ "message": "Провери са Duo Security за вашу организацију користећи Duo Mobile апликацију, СМС, телефонски позив, или U2F кључ.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index e26e44c6923..2db2418aae0 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -885,6 +885,15 @@ "message": "Verifiera med Duo Security för din organisation genom att använda Duo Mobile-appen, SMS, telefonsamtal eller en U2F-säkerhetsnyckel.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index bca12f16a7d..4f18a88d994 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -885,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index 3298ed16682..a2b93eb4086 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -885,6 +885,15 @@ "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 76ba6cb9d2d..b5ec934712d 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -885,6 +885,15 @@ "message": "Kuruluşunuzun Duo Security doğrulaması için Duo Mobile uygulaması, SMS, telefon görüşmesi veya U2F güvenlik anahtarını kullanın.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Kimliğinizi doğrulayın" + }, + "weDontRecognizeThisDevice": { + "message": "Bu cihazı tanıyamadık. Kimliğinizi doğrulamak için e-postanıza gönderilen kodu girin." + }, + "continueLoggingIn": { + "message": "Giriş yapmaya devam et" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Uzantıyı güncellemeniz gerekiyor" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "Kullandığınız tarayı uzantısı eskimiş. Lütfen uzantıyı güncelleyin veya masaüstü uygulamasının ayarlarından parmak izi doğrulaması için tarayıcı entegrasyonunu kapatın." } } diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 4d9e8512421..de8fd8664ca 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -250,17 +250,17 @@ "message": "Помилка" }, "decryptionError": { - "message": "Decryption error" + "message": "Помилка розшифрування" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden не зміг розшифрувати вказані нижче елементи сховища." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "Зверніться до служби підтримки клієнтів,", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "щоб уникнути втрати даних.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "january": { @@ -885,6 +885,15 @@ "message": "Авторизуйтесь за допомогою Duo Security для вашої організації з використанням програми Duo Mobile, SMS, телефонного виклику, або ключа безпеки U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Підтвердьте свою особу" + }, + "weDontRecognizeThisDevice": { + "message": "Ми не розпізнаємо цей пристрій. Введіть код, надісланий на вашу електронну пошту, щоб підтвердити вашу особу." + }, + "continueLoggingIn": { + "message": "Продовжити вхід" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2880,10 +2889,10 @@ "message": "Виявлено слабкий пароль, який знайдено у витоку даних. Використовуйте надійний та унікальний пароль для захисту свого облікового запису. Ви дійсно хочете використати цей пароль?" }, "useThisPassword": { - "message": "Use this password" + "message": "Використати цей пароль" }, "useThisUsername": { - "message": "Use this username" + "message": "Використати це ім'я користувача" }, "checkForBreaches": { "message": "Перевірити відомі витоки даних для цього пароля" @@ -3383,19 +3392,19 @@ "message": "Не знайдено вільних портів для цього входу SSO." }, "biometricsStatusHelptextUnlockNeeded": { - "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + "message": "Біометричне розблокування недоступне, оскільки спочатку потрібно ввести PIN-код або пароль." }, "biometricsStatusHelptextHardwareUnavailable": { - "message": "Biometric unlock is currently unavailable." + "message": "Біометричне розблокування наразі недоступне." }, "biometricsStatusHelptextAutoSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Біометричне розблокування недоступне через неправильно налаштовані системні файли." }, "biometricsStatusHelptextManualSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Біометричне розблокування недоступне через неправильно налаштовані системні файли." }, "biometricsStatusHelptextNotEnabledLocally": { - "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "message": "Біометричне розблокування недоступне, оскільки воно не увімкнене для $EMAIL$ у програмі Bitwarden для комп'ютера.", "placeholders": { "email": { "content": "$1", @@ -3404,7 +3413,7 @@ } }, "biometricsStatusHelptextUnavailableReasonUnknown": { - "message": "Biometric unlock is currently unavailable for an unknown reason." + "message": "Біометричне розблокування зараз недоступне з невідомої причини." }, "authorize": { "message": "Авторизувати" @@ -3476,12 +3485,18 @@ "message": "Змінити адресу е-пошти" }, "organizationUpgradeRequired": { - "message": "Organization upgrade required" + "message": "Необхідне оновлення організації" }, "upgradeOrganization": { - "message": "Upgrade organization" + "message": "Підвищити рівень організації" }, "upgradeOrganizationDesc": { - "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + "message": "Ця функція недоступна для безплатних організацій. Передплатіть тарифний план, щоб розблокувати додаткові можливості." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Необхідно оновити розширення" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "Ви використовуєте застарілу версію розширення браузера. Оновіть його або вимкніть перевірку цифрового відбитка інтеграції з браузером у налаштуваннях комп'ютерної програми." } } diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index c0e79109ad8..4f996404b8a 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -885,6 +885,15 @@ "message": "Xác minh với Duo Security cho tổ chức của bạn sử dụng ứng dụng Duo Mobile, SMS, cuộc gọi điện thoại, hoặc khoá bảo mật U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 54b51259a8b..45ece148eeb 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -885,6 +885,15 @@ "message": "为您的组织使用 Duo Security 的 Duo 移动 App、短信、电话或 U2F 安全钥匙来进行验证。", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "验证您的身份" + }, + "weDontRecognizeThisDevice": { + "message": "我们无法识别这个设备。请输入发送到您电子邮箱中的代码以验证您的身份。" + }, + "continueLoggingIn": { + "message": "继续登录" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2125,11 +2134,11 @@ "message": "当前访问次数" }, "disableSend": { - "message": "停用此 Send 则任何人无法访问它。", + "message": "停用此 Send 确保无人能访问它。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendPasswordDesc": { - "message": "可选,用户需要提供密码才能访问此 Send。", + "message": "可选。用户需要提供密码才能访问此 Send。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2172,10 +2181,10 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTextDesc": { - "message": "您想要发送的文本。" + "message": "您想在此 Send 中附加的文本。" }, "sendFileDesc": { - "message": "您想要发送的文件。" + "message": "您想在此 Send 中附加的文件。" }, "days": { "message": "$DAYS$ 天", @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "此功能不适用于免费组织。请切换到付费计划以解锁更多功能。" + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "扩展需要更新" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "您正在使用的浏览器扩展已过时。请更新它或在桌面 App 的设置中禁用浏览器集成指纹验证。" } } diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index c59d3f8ddfb..ecbdf0cbf96 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -885,6 +885,15 @@ "message": "爲您的組織使用 Duo Security 的 Duo Mobile 程式、SMS、致電或 U2F 安全鑰匙進行驗證。", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, + "verifyIdentity": { + "message": "Verify your Identity" + }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, @@ -2740,13 +2749,13 @@ "message": "已傳送通知至您的裝置。" }, "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "message": "已傳送通知至您的裝置" }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "需要另一個選項嗎?" }, "fingerprintMatchInfo": { "message": "請確保您的密碼庫已解鎖,並且指紋短語與其他裝置的一致。" @@ -2761,7 +2770,7 @@ "message": "必須先在 Bitwarden 應用程式設定中開啟,才可以使用裝置登入。要改用其他選項嗎?" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "檢視所有登入選項" }, "viewAllLoginOptions": { "message": "檢視所有登入選項" @@ -2862,7 +2871,7 @@ "message": "No email?" }, "goBack": { - "message": "Go back" + "message": "返回" }, "toEditYourEmailAddress": { "message": "to edit your email address." @@ -2880,16 +2889,16 @@ "message": "密碼強度不足,且該密碼已洩露。請使用一個強度足夠和獨特的密碼來保護您的帳戶。您確定要使用這個密碼嗎?" }, "useThisPassword": { - "message": "Use this password" + "message": "使用此密碼" }, "useThisUsername": { - "message": "Use this username" + "message": "使用此使用者名稱" }, "checkForBreaches": { "message": "檢查外洩的密碼資料庫中是否包含此密碼" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "已登入!" }, "important": { "message": "重要:" @@ -3483,5 +3492,11 @@ }, "upgradeOrganizationDesc": { "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." + }, + "updateBrowserOrDisableFingerprintDialogTitle": { + "message": "Extension update required" + }, + "updateBrowserOrDisableFingerprintDialogMessage": { + "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." } } diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index f3dc98b8d9b..459785f59d3 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2025.1.4", + "version": "2025.1.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2025.1.4", + "version": "2025.1.8", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native/napi" diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 7497d31d621..db546590ba5 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2025.1.4", + "version": "2025.1.8", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/web/package.json b/apps/web/package.json index 73ecd8c3cb7..7047b0fb137 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2025.1.1", + "version": "2025.1.2", "scripts": { "build:oss": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", "build:bit": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/apps/web/src/app/admin-console/common/base.people.component.ts b/apps/web/src/app/admin-console/common/base.people.component.ts index 10632582e99..07ae67ac33b 100644 --- a/apps/web/src/app/admin-console/common/base.people.component.ts +++ b/apps/web/src/app/admin-console/common/base.people.component.ts @@ -6,7 +6,6 @@ import { firstValueFrom, concatMap, map, lastValueFrom, startWith, debounceTime import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; @@ -123,7 +122,6 @@ export abstract class BasePeopleComponent< protected platformUtilsService: PlatformUtilsService, protected keyService: KeyService, protected validationService: ValidationService, - protected modalService: ModalService, private logService: LogService, private searchPipe: SearchPipe, protected userNamePipe: UserNamePipe, diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html index 5e81e4ee711..bef479c231b 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html @@ -2,9 +2,11 @@ {{ title }} - {{ - params.name - }} + {{ (editParams$ | async)?.name }} {{ "revoked" | i18n }}
@@ -268,7 +270,9 @@

{{ "paymentType" | i18n }}

- - +
diff --git a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts index 213f051f75e..1c24bb9fd80 100644 --- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts +++ b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts @@ -13,14 +13,12 @@ import { } from "@bitwarden/common/billing/abstractions/organization-billing.service"; import { PaymentMethodType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; -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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { ToastService } from "@bitwarden/components"; -import { BillingSharedModule, PaymentComponent, TaxInfoComponent } from "../../shared"; -import { PaymentV2Component } from "../../shared/payment/payment-v2.component"; +import { BillingSharedModule, TaxInfoComponent } from "../../shared"; +import { PaymentComponent } from "../../shared/payment/payment.component"; export type TrialOrganizationType = Exclude; @@ -53,7 +51,6 @@ export enum SubscriptionProduct { }) export class TrialBillingStepComponent implements OnInit { @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; - @ViewChild(PaymentV2Component) paymentV2Component: PaymentV2Component; @ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent; @Input() organizationInfo: OrganizationInfo; @Input() subscriptionProduct: SubscriptionProduct = SubscriptionProduct.PasswordManager; @@ -74,11 +71,8 @@ export class TrialBillingStepComponent implements OnInit { annualPlan?: PlanResponse; monthlyPlan?: PlanResponse; - deprecateStripeSourcesAPI: boolean; - constructor( private apiService: ApiService, - private configService: ConfigService, private i18nService: I18nService, private formBuilder: FormBuilder, private messagingService: MessagingService, @@ -87,9 +81,6 @@ export class TrialBillingStepComponent implements OnInit { ) {} async ngOnInit(): Promise { - this.deprecateStripeSourcesAPI = await this.configService.getFeatureFlag( - FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - ); const plans = await this.apiService.getPlans(); this.applicablePlans = plans.data.filter(this.isApplicable); this.annualPlan = this.findPlanFor(SubscriptionCadence.Annual); @@ -124,23 +115,12 @@ export class TrialBillingStepComponent implements OnInit { } protected changedCountry() { - if (this.deprecateStripeSourcesAPI) { - this.paymentV2Component.showBankAccount = this.taxInfoComponent.country === "US"; - if ( - !this.paymentV2Component.showBankAccount && - this.paymentV2Component.selected === PaymentMethodType.BankAccount - ) { - this.paymentV2Component.select(PaymentMethodType.Card); - } - } else { - this.paymentComponent.hideBank = this.taxInfoComponent.taxFormGroup.value.country !== "US"; - if ( - this.paymentComponent.hideBank && - this.paymentComponent.method === PaymentMethodType.BankAccount - ) { - this.paymentComponent.method = PaymentMethodType.Card; - this.paymentComponent.changeMethod(); - } + this.paymentComponent.showBankAccount = this.taxInfoComponent.country === "US"; + if ( + !this.paymentComponent.showBankAccount && + this.paymentComponent.selected === PaymentMethodType.BankAccount + ) { + this.paymentComponent.select(PaymentMethodType.Card); } } @@ -162,14 +142,8 @@ export class TrialBillingStepComponent implements OnInit { private async createOrganization(): Promise { const planResponse = this.findPlanFor(this.formGroup.value.cadence); - let paymentMethod: [string, PaymentMethodType]; - - if (this.deprecateStripeSourcesAPI) { - const { type, token } = await this.paymentV2Component.tokenize(); - paymentMethod = [token, type]; - } else { - paymentMethod = await this.paymentComponent.createPaymentToken(); - } + const { type, token } = await this.paymentComponent.tokenize(); + const paymentMethod: [string, PaymentMethodType] = [token, type]; const organization: OrganizationInformation = { name: this.organizationInfo.name, diff --git a/apps/web/src/app/billing/index.ts b/apps/web/src/app/billing/index.ts index b59ab33e54c..217f1e05be9 100644 --- a/apps/web/src/app/billing/index.ts +++ b/apps/web/src/app/billing/index.ts @@ -1,2 +1,2 @@ export { OrganizationPlansComponent } from "./organizations"; -export { PaymentComponent, TaxInfoComponent } from "./shared"; +export { TaxInfoComponent } from "./shared"; diff --git a/apps/web/src/app/billing/individual/individual-billing-routing.module.ts b/apps/web/src/app/billing/individual/individual-billing-routing.module.ts index 585d9b418c1..bb1ada0b719 100644 --- a/apps/web/src/app/billing/individual/individual-billing-routing.module.ts +++ b/apps/web/src/app/billing/individual/individual-billing-routing.module.ts @@ -1,13 +1,9 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; -import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; - import { PaymentMethodComponent } from "../shared"; import { BillingHistoryViewComponent } from "./billing-history-view.component"; -import { PremiumV2Component } from "./premium/premium-v2.component"; import { PremiumComponent } from "./premium/premium.component"; import { SubscriptionComponent } from "./subscription.component"; import { UserSubscriptionComponent } from "./user-subscription.component"; @@ -24,15 +20,11 @@ const routes: Routes = [ component: UserSubscriptionComponent, data: { titleId: "premiumMembership" }, }, - ...featureFlaggedRoute({ - defaultComponent: PremiumComponent, - flaggedComponent: PremiumV2Component, - featureFlag: FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - routeOptions: { - path: "premium", - data: { titleId: "goPremium" }, - }, - }), + { + path: "premium", + component: PremiumComponent, + data: { titleId: "goPremium" }, + }, { path: "payment-method", component: PaymentMethodComponent, diff --git a/apps/web/src/app/billing/individual/individual-billing.module.ts b/apps/web/src/app/billing/individual/individual-billing.module.ts index 0dbbc8c6837..ad75da00c99 100644 --- a/apps/web/src/app/billing/individual/individual-billing.module.ts +++ b/apps/web/src/app/billing/individual/individual-billing.module.ts @@ -5,7 +5,6 @@ import { BillingSharedModule } from "../shared"; import { BillingHistoryViewComponent } from "./billing-history-view.component"; import { IndividualBillingRoutingModule } from "./individual-billing-routing.module"; -import { PremiumV2Component } from "./premium/premium-v2.component"; import { PremiumComponent } from "./premium/premium.component"; import { SubscriptionComponent } from "./subscription.component"; import { UserSubscriptionComponent } from "./user-subscription.component"; @@ -17,7 +16,6 @@ import { UserSubscriptionComponent } from "./user-subscription.component"; BillingHistoryViewComponent, UserSubscriptionComponent, PremiumComponent, - PremiumV2Component, ], }) export class IndividualBillingModule {} diff --git a/apps/web/src/app/billing/individual/premium/premium-v2.component.html b/apps/web/src/app/billing/individual/premium/premium-v2.component.html deleted file mode 100644 index 7adc93fd962..00000000000 --- a/apps/web/src/app/billing/individual/premium/premium-v2.component.html +++ /dev/null @@ -1,149 +0,0 @@ - -

{{ "goPremium" | i18n }}

- - {{ "alreadyPremiumFromOrg" | i18n }} - - -

{{ "premiumUpgradeUnlockFeatures" | i18n }}

-
    -
  • - - {{ "premiumSignUpStorage" | i18n }} -
  • -
  • - - {{ "premiumSignUpTwoStepOptions" | i18n }} -
  • -
  • - - {{ "premiumSignUpEmergency" | i18n }} -
  • -
  • - - {{ "premiumSignUpReports" | i18n }} -
  • -
  • - - {{ "premiumSignUpTotp" | i18n }} -
  • -
  • - - {{ "premiumSignUpSupport" | i18n }} -
  • -
  • - - {{ "premiumSignUpFuture" | i18n }} -
  • -
-

- {{ - "premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount - }} - - {{ "bitwardenFamiliesPlan" | i18n }} - -

- - {{ "purchasePremium" | i18n }} - -
-
- - -

{{ "uploadLicenseFilePremium" | i18n }}

-
- - {{ "licenseFile" | i18n }} -
- - {{ - licenseFormGroup.value.file ? licenseFormGroup.value.file.name : ("noFileChosen" | i18n) - }} -
- - {{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }} -
- -
-
- -
-
- -

{{ "addons" | i18n }}

-
- - {{ "additionalStorageGb" | i18n }} - - {{ - "additionalStorageIntervalDesc" - | i18n: "1 GB" : (storageGBPrice | currency: "$") : ("year" | i18n) - }} - -
-
- -

{{ "summary" | i18n }}

- {{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }}
- {{ "additionalStorageGb" | i18n }}: {{ addOnFormGroup.value.additionalStorage || 0 }} GB × - {{ storageGBPrice | currency: "$" }} = - {{ additionalStorageCost | currency: "$" }} -
-
- -

{{ "paymentInformation" | i18n }}

- - -
-
- {{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }} - {{ "estimatedTax" | i18n }}: {{ estimatedTax | currency: "USD $" }} -
-
-
-

- {{ "total" | i18n }}: {{ total | currency: "USD $" }}/{{ "year" | i18n }} -

- -
-
diff --git a/apps/web/src/app/billing/individual/premium/premium-v2.component.ts b/apps/web/src/app/billing/individual/premium/premium-v2.component.ts deleted file mode 100644 index 11b55f92b40..00000000000 --- a/apps/web/src/app/billing/individual/premium/premium-v2.component.ts +++ /dev/null @@ -1,228 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, ViewChild } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; -import { combineLatest, concatMap, from, Observable, of, switchMap } from "rxjs"; -import { debounceTime } from "rxjs/operators"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; -import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { PreviewIndividualInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-individual-invoice.request"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SyncService } from "@bitwarden/common/platform/sync"; -import { ToastService } from "@bitwarden/components"; - -import { PaymentV2Component } from "../../shared/payment/payment-v2.component"; -import { TaxInfoComponent } from "../../shared/tax-info.component"; - -@Component({ - templateUrl: "./premium-v2.component.html", -}) -export class PremiumV2Component { - @ViewChild(PaymentV2Component) paymentComponent: PaymentV2Component; - @ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent; - - protected hasPremiumFromAnyOrganization$: Observable; - - protected addOnFormGroup = new FormGroup({ - additionalStorage: new FormControl(0, [Validators.min(0), Validators.max(99)]), - }); - - protected licenseFormGroup = new FormGroup({ - file: new FormControl(null, [Validators.required]), - }); - - protected cloudWebVaultURL: string; - protected isSelfHost = false; - - protected useLicenseUploaderComponent$ = this.configService.getFeatureFlag$( - FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader, - ); - - protected estimatedTax: number = 0; - protected readonly familyPlanMaxUserCount = 6; - protected readonly premiumPrice = 10; - protected readonly storageGBPrice = 4; - - constructor( - private activatedRoute: ActivatedRoute, - private apiService: ApiService, - private billingAccountProfileStateService: BillingAccountProfileStateService, - private configService: ConfigService, - private environmentService: EnvironmentService, - private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private router: Router, - private syncService: SyncService, - private toastService: ToastService, - private tokenService: TokenService, - private taxService: TaxServiceAbstraction, - private accountService: AccountService, - ) { - this.isSelfHost = this.platformUtilsService.isSelfHost(); - - this.hasPremiumFromAnyOrganization$ = this.accountService.activeAccount$.pipe( - switchMap((account) => - this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id), - ), - ); - - combineLatest([ - this.accountService.activeAccount$.pipe( - switchMap((account) => - this.billingAccountProfileStateService.hasPremiumPersonally$(account.id), - ), - ), - this.environmentService.cloudWebVaultUrl$, - ]) - .pipe( - takeUntilDestroyed(), - concatMap(([hasPremiumPersonally, cloudWebVaultURL]) => { - if (hasPremiumPersonally) { - return from(this.navigateToSubscriptionPage()); - } - - this.cloudWebVaultURL = cloudWebVaultURL; - return of(true); - }), - ) - .subscribe(); - - this.addOnFormGroup.controls.additionalStorage.valueChanges - .pipe(debounceTime(1000), takeUntilDestroyed()) - .subscribe(() => { - this.refreshSalesTax(); - }); - } - - finalizeUpgrade = async () => { - await this.apiService.refreshIdentityToken(); - await this.syncService.fullSync(true); - }; - - postFinalizeUpgrade = async () => { - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("premiumUpdated"), - }); - await this.navigateToSubscriptionPage(); - }; - - navigateToSubscriptionPage = (): Promise => - this.router.navigate(["../user-subscription"], { relativeTo: this.activatedRoute }); - - onLicenseFileSelected = (event: Event): void => { - const element = event.target as HTMLInputElement; - this.licenseFormGroup.value.file = element.files.length > 0 ? element.files[0] : null; - }; - - submitPremiumLicense = async (): Promise => { - this.licenseFormGroup.markAllAsTouched(); - - if (this.licenseFormGroup.invalid) { - return this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("selectFile"), - }); - } - - const emailVerified = await this.tokenService.getEmailVerified(); - if (!emailVerified) { - return this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("verifyEmailFirst"), - }); - } - - const formData = new FormData(); - formData.append("license", this.licenseFormGroup.value.file); - - await this.apiService.postAccountLicense(formData); - await this.finalizeUpgrade(); - await this.postFinalizeUpgrade(); - }; - - submitPayment = async (): Promise => { - this.taxInfoComponent.taxFormGroup.markAllAsTouched(); - if (this.taxInfoComponent.taxFormGroup.invalid) { - return; - } - - const { type, token } = await this.paymentComponent.tokenize(); - - const formData = new FormData(); - formData.append("paymentMethodType", type.toString()); - formData.append("paymentToken", token); - formData.append("additionalStorageGb", this.addOnFormGroup.value.additionalStorage.toString()); - formData.append("country", this.taxInfoComponent.country); - formData.append("postalCode", this.taxInfoComponent.postalCode); - - await this.apiService.postPremium(formData); - await this.finalizeUpgrade(); - await this.postFinalizeUpgrade(); - }; - - protected get additionalStorageCost(): number { - return this.storageGBPrice * this.addOnFormGroup.value.additionalStorage; - } - - protected get premiumURL(): string { - return `${this.cloudWebVaultURL}/#/settings/subscription/premium`; - } - - protected get subtotal(): number { - return this.premiumPrice + this.additionalStorageCost; - } - - protected get total(): number { - return this.subtotal + this.estimatedTax; - } - - protected async onLicenseFileSelectedChanged(): Promise { - await this.postFinalizeUpgrade(); - } - - private refreshSalesTax(): void { - if (!this.taxInfoComponent.country || !this.taxInfoComponent.postalCode) { - return; - } - const request: PreviewIndividualInvoiceRequest = { - passwordManager: { - additionalStorage: this.addOnFormGroup.value.additionalStorage, - }, - taxInformation: { - postalCode: this.taxInfoComponent.postalCode, - country: this.taxInfoComponent.country, - }, - }; - - this.taxService - .previewIndividualInvoice(request) - .then((invoice) => { - this.estimatedTax = invoice.taxAmount; - }) - .catch((error) => { - this.toastService.showToast({ - title: "", - variant: "error", - message: this.i18nService.t(error.message), - }); - }); - } - - protected onTaxInformationChanged(): void { - this.refreshSalesTax(); - } -} diff --git a/apps/web/src/app/billing/individual/premium/premium.component.html b/apps/web/src/app/billing/individual/premium/premium.component.html index 12b6932d0f5..acf24ed2a34 100644 --- a/apps/web/src/app/billing/individual/premium/premium.component.html +++ b/apps/web/src/app/billing/individual/premium/premium.component.html @@ -1,8 +1,8 @@ -

{{ "goPremium" | i18n }}

+

{{ "goPremium" | i18n }}

@@ -40,7 +40,7 @@

{{ "goPremium" | i18n }}

{{ "premiumSignUpFuture" | i18n }} -

+

{{ "premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount }} @@ -49,49 +49,58 @@

{{ "goPremium" | i18n }}

linkType="primary" routerLink="/create-organization" [queryParams]="{ plan: 'families' }" - >{{ "bitwardenFamiliesPlan" | i18n }} + {{ "bitwardenFamiliesPlan" | i18n }} +

{{ "purchasePremium" | i18n }}
- -

{{ "uploadLicenseFilePremium" | i18n }}

-
- - {{ "licenseFile" | i18n }} -
- - {{ this.licenseFile ? this.licenseFile.name : ("noFileChosen" | i18n) }} -
- - {{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }} -
- -
+ + +

{{ "uploadLicenseFilePremium" | i18n }}

+
+ + {{ "licenseFile" | i18n }} +
+ + {{ + licenseFormGroup.value.file ? licenseFormGroup.value.file.name : ("noFileChosen" | i18n) + }} +
+ + {{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }} +
+ +
+
+
-
+

{{ "addons" | i18n }}

@@ -106,7 +115,7 @@

{{ "addons" | i18n }}

/> {{ "additionalStorageIntervalDesc" - | i18n: "1 GB" : (storageGbPrice | currency: "$") : ("year" | i18n) + | i18n: "1 GB" : (storageGBPrice | currency: "$") : ("year" | i18n) }}
@@ -114,30 +123,26 @@

{{ "addons" | i18n }}

{{ "summary" | i18n }}

{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }}
- {{ "additionalStorageGb" | i18n }}: {{ additionalStorage || 0 }} GB × - {{ storageGbPrice | currency: "$" }} = - {{ additionalStorageTotal | currency: "$" }} + {{ "additionalStorageGb" | i18n }}: {{ addOnFormGroup.value.additionalStorage || 0 }} GB × + {{ storageGBPrice | currency: "$" }} = + {{ additionalStorageCost | currency: "$" }}

{{ "paymentInformation" | i18n }}

- - -
-
- {{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }} -
- - {{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }} - + + +
+
+ {{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }} + {{ "estimatedTax" | i18n }}: {{ estimatedTax | currency: "USD $" }}
-
-

- {{ "total" | i18n }}: {{ total | currency: "USD $" }}/{{ "year" | i18n }} -

-

{{ "paymentChargedAnnually" | i18n }}

- diff --git a/apps/web/src/app/billing/individual/premium/premium.component.ts b/apps/web/src/app/billing/individual/premium/premium.component.ts index f96f573cd4d..ec19eb02594 100644 --- a/apps/web/src/app/billing/individual/premium/premium.component.ts +++ b/apps/web/src/app/billing/individual/premium/premium.component.ts @@ -1,187 +1,197 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, OnInit, ViewChild } from "@angular/core"; +import { Component, ViewChild } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { Router } from "@angular/router"; -import { firstValueFrom, Observable, switchMap } from "rxjs"; +import { ActivatedRoute, Router } from "@angular/router"; +import { combineLatest, concatMap, from, Observable, of, switchMap } from "rxjs"; import { debounceTime } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; import { PreviewIndividualInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-individual-invoice.request"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { ToastService } from "@bitwarden/components"; -import { PaymentComponent, TaxInfoComponent } from "../../shared"; +import { PaymentComponent } from "../../shared/payment/payment.component"; +import { TaxInfoComponent } from "../../shared/tax-info.component"; @Component({ - templateUrl: "premium.component.html", + templateUrl: "./premium.component.html", }) -export class PremiumComponent implements OnInit { +export class PremiumComponent { @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; @ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent; - canAccessPremium$: Observable; - selfHosted = false; - premiumPrice = 10; - familyPlanMaxUserCount = 6; - storageGbPrice = 4; - cloudWebVaultUrl: string; - licenseFile: File = null; - - formPromise: Promise; - protected licenseForm = new FormGroup({ - file: new FormControl(null, [Validators.required]), + protected hasPremiumFromAnyOrganization$: Observable; + + protected addOnFormGroup = new FormGroup({ + additionalStorage: new FormControl(0, [Validators.min(0), Validators.max(99)]), }); - protected addonForm = new FormGroup({ - additionalStorage: new FormControl(0, [Validators.max(99), Validators.min(0)]), + + protected licenseFormGroup = new FormGroup({ + file: new FormControl(null, [Validators.required]), }); - private estimatedTax: number = 0; + protected cloudWebVaultURL: string; + protected isSelfHost = false; + + protected useLicenseUploaderComponent$ = this.configService.getFeatureFlag$( + FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader, + ); + + protected estimatedTax: number = 0; + protected readonly familyPlanMaxUserCount = 6; + protected readonly premiumPrice = 10; + protected readonly storageGBPrice = 4; constructor( + private activatedRoute: ActivatedRoute, private apiService: ApiService, + private billingAccountProfileStateService: BillingAccountProfileStateService, + private configService: ConfigService, + private environmentService: EnvironmentService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, - private tokenService: TokenService, private router: Router, - private messagingService: MessagingService, private syncService: SyncService, - private environmentService: EnvironmentService, - private billingAccountProfileStateService: BillingAccountProfileStateService, private toastService: ToastService, + private tokenService: TokenService, private taxService: TaxServiceAbstraction, private accountService: AccountService, ) { - this.selfHosted = platformUtilsService.isSelfHost(); - this.canAccessPremium$ = this.accountService.activeAccount$.pipe( + this.isSelfHost = this.platformUtilsService.isSelfHost(); + + this.hasPremiumFromAnyOrganization$ = this.accountService.activeAccount$.pipe( switchMap((account) => - this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id), ), ); - this.addonForm.controls.additionalStorage.valueChanges + combineLatest([ + this.accountService.activeAccount$.pipe( + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumPersonally$(account.id), + ), + ), + this.environmentService.cloudWebVaultUrl$, + ]) + .pipe( + takeUntilDestroyed(), + concatMap(([hasPremiumPersonally, cloudWebVaultURL]) => { + if (hasPremiumPersonally) { + return from(this.navigateToSubscriptionPage()); + } + + this.cloudWebVaultURL = cloudWebVaultURL; + return of(true); + }), + ) + .subscribe(); + + this.addOnFormGroup.controls.additionalStorage.valueChanges .pipe(debounceTime(1000), takeUntilDestroyed()) .subscribe(() => { this.refreshSalesTax(); }); } - protected setSelectedFile(event: Event) { - const fileInputEl = event.target; - const file: File = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null; - this.licenseFile = file; - } - async ngOnInit() { - this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$); - const account = await firstValueFrom(this.accountService.activeAccount$); - if ( - await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$(account.id)) - ) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/settings/subscription/user-subscription"]); - return; - } - } - submit = async () => { - if (this.taxInfoComponent) { - if (!this.taxInfoComponent?.taxFormGroup.valid) { - this.taxInfoComponent.taxFormGroup.markAllAsTouched(); - return; - } - } - this.licenseForm.markAllAsTouched(); - this.addonForm.markAllAsTouched(); - if (this.selfHosted) { - if (this.licenseFile == null) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("selectFile"), - }); - return; - } - } - - if (this.selfHosted) { - // eslint-disable-next-line @typescript-eslint/no-misused-promises - if (!this.tokenService.getEmailVerified()) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("verifyEmailFirst"), - }); - return; - } - - const fd = new FormData(); - fd.append("license", this.licenseFile); - await this.apiService.postAccountLicense(fd).then(() => { - return this.finalizePremium(); - }); - } else { - await this.paymentComponent - .createPaymentToken() - .then((result) => { - const fd = new FormData(); - fd.append("paymentMethodType", result[1].toString()); - if (result[0] != null) { - fd.append("paymentToken", result[0]); - } - fd.append("additionalStorageGb", (this.additionalStorage || 0).toString()); - fd.append("country", this.taxInfoComponent?.taxFormGroup?.value.country); - fd.append("postalCode", this.taxInfoComponent?.taxFormGroup?.value.postalCode); - return this.apiService.postPremium(fd); - }) - .then((paymentResponse) => { - if (!paymentResponse.success && paymentResponse.paymentIntentClientSecret != null) { - return this.paymentComponent.handleStripeCardPayment( - paymentResponse.paymentIntentClientSecret, - () => this.finalizePremium(), - ); - } else { - return this.finalizePremium(); - } - }); - } - }; - async finalizePremium() { + finalizeUpgrade = async () => { await this.apiService.refreshIdentityToken(); await this.syncService.fullSync(true); + }; + + postFinalizeUpgrade = async () => { this.toastService.showToast({ variant: "success", title: null, message: this.i18nService.t("premiumUpdated"), }); - await this.router.navigate(["/settings/subscription/user-subscription"]); - } + await this.navigateToSubscriptionPage(); + }; + + navigateToSubscriptionPage = (): Promise => + this.router.navigate(["../user-subscription"], { relativeTo: this.activatedRoute }); + + onLicenseFileSelected = (event: Event): void => { + const element = event.target as HTMLInputElement; + this.licenseFormGroup.value.file = element.files.length > 0 ? element.files[0] : null; + }; + + submitPremiumLicense = async (): Promise => { + this.licenseFormGroup.markAllAsTouched(); + + if (this.licenseFormGroup.invalid) { + return this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("selectFile"), + }); + } + + const emailVerified = await this.tokenService.getEmailVerified(); + if (!emailVerified) { + return this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("verifyEmailFirst"), + }); + } - get additionalStorage(): number { - return this.addonForm.get("additionalStorage").value; + const formData = new FormData(); + formData.append("license", this.licenseFormGroup.value.file); + + await this.apiService.postAccountLicense(formData); + await this.finalizeUpgrade(); + await this.postFinalizeUpgrade(); + }; + + submitPayment = async (): Promise => { + this.taxInfoComponent.taxFormGroup.markAllAsTouched(); + if (this.taxInfoComponent.taxFormGroup.invalid) { + return; + } + + const { type, token } = await this.paymentComponent.tokenize(); + + const formData = new FormData(); + formData.append("paymentMethodType", type.toString()); + formData.append("paymentToken", token); + formData.append("additionalStorageGb", this.addOnFormGroup.value.additionalStorage.toString()); + formData.append("country", this.taxInfoComponent.country); + formData.append("postalCode", this.taxInfoComponent.postalCode); + + await this.apiService.postPremium(formData); + await this.finalizeUpgrade(); + await this.postFinalizeUpgrade(); + }; + + protected get additionalStorageCost(): number { + return this.storageGBPrice * this.addOnFormGroup.value.additionalStorage; } - get additionalStorageTotal(): number { - return this.storageGbPrice * Math.abs(this.additionalStorage || 0); + + protected get premiumURL(): string { + return `${this.cloudWebVaultURL}/#/settings/subscription/premium`; } - get subtotal(): number { - return this.premiumPrice + this.additionalStorageTotal; + protected get subtotal(): number { + return this.premiumPrice + this.additionalStorageCost; } - get taxCharges(): number { - return this.estimatedTax; + protected get total(): number { + return this.subtotal + this.estimatedTax; } - get total(): number { - return this.subtotal + this.taxCharges || 0; + protected async onLicenseFileSelectedChanged(): Promise { + await this.postFinalizeUpgrade(); } private refreshSalesTax(): void { @@ -190,7 +200,7 @@ export class PremiumComponent implements OnInit { } const request: PreviewIndividualInvoiceRequest = { passwordManager: { - additionalStorage: this.addonForm.value.additionalStorage, + additionalStorage: this.addOnFormGroup.value.additionalStorage, }, taxInformation: { postalCode: this.taxInfoComponent.postalCode, diff --git a/apps/web/src/app/billing/individual/user-subscription.component.ts b/apps/web/src/app/billing/individual/user-subscription.component.ts index 97b4725e6d7..38f4436fb47 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.ts +++ b/apps/web/src/app/billing/individual/user-subscription.component.ts @@ -8,8 +8,6 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -18,12 +16,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { DialogService, ToastService } from "@bitwarden/components"; import { - AdjustStorageDialogV2Component, - AdjustStorageDialogV2ResultType, -} from "../shared/adjust-storage-dialog/adjust-storage-dialog-v2.component"; -import { - AdjustStorageDialogResult, - openAdjustStorageDialog, + AdjustStorageDialogComponent, + AdjustStorageDialogResultType, } from "../shared/adjust-storage-dialog/adjust-storage-dialog.component"; import { OffboardingSurveyDialogResultType, @@ -45,10 +39,6 @@ export class UserSubscriptionComponent implements OnInit { cancelPromise: Promise; reinstatePromise: Promise; - protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$( - FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - ); - constructor( private apiService: ApiService, private platformUtilsService: PlatformUtilsService, @@ -60,7 +50,6 @@ export class UserSubscriptionComponent implements OnInit { private environmentService: EnvironmentService, private billingAccountProfileStateService: BillingAccountProfileStateService, private toastService: ToastService, - private configService: ConfigService, private accountService: AccountService, ) { this.selfHosted = this.platformUtilsService.isSelfHost(); @@ -166,33 +155,18 @@ export class UserSubscriptionComponent implements OnInit { }; adjustStorage = async (add: boolean) => { - const deprecateStripeSourcesAPI = await firstValueFrom(this.deprecateStripeSourcesAPI$); - - if (deprecateStripeSourcesAPI) { - const dialogRef = AdjustStorageDialogV2Component.open(this.dialogService, { - data: { - price: 4, - cadence: "year", - type: add ? "Add" : "Remove", - }, - }); + const dialogRef = AdjustStorageDialogComponent.open(this.dialogService, { + data: { + price: 4, + cadence: "year", + type: add ? "Add" : "Remove", + }, + }); - const result = await lastValueFrom(dialogRef.closed); + const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustStorageDialogV2ResultType.Submitted) { - await this.load(); - } - } else { - const dialogRef = openAdjustStorageDialog(this.dialogService, { - data: { - storageGbPrice: 4, - add: add, - }, - }); - const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustStorageDialogResult.Adjusted) { - await this.load(); - } + if (result === AdjustStorageDialogResultType.Submitted) { + await this.load(); } }; diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html index 96679ea1753..b5471a90fd5 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html @@ -346,25 +346,20 @@

{{ "paymentMethod" | i18n }}

" > - {{ - deprecateStripeSourcesAPI - ? paymentSource?.description - : billing?.paymentSource?.description - }} + {{ paymentSource?.description }} {{ "changePaymentMethod" | i18n }}

- - + -
+

{{ "total" | i18n }}: @@ -962,7 +957,7 @@

{{ "paymentMethod" | i18n }}

-
+

; - deprecateStripeSourcesAPI: boolean; isSubscriptionCanceled: boolean = false; private destroy$ = new Subject(); @@ -210,7 +203,6 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { private messagingService: MessagingService, private formBuilder: FormBuilder, private organizationApiService: OrganizationApiServiceAbstraction, - private configService: ConfigService, private billingApiService: BillingApiServiceAbstraction, private taxService: TaxServiceAbstraction, private accountService: AccountService, @@ -218,10 +210,6 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { ) {} async ngOnInit(): Promise { - this.deprecateStripeSourcesAPI = await this.configService.getFeatureFlag( - FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - ); - if (this.dialogParams.organizationId) { this.currentPlanName = this.resolvePlanName(this.dialogParams.productTierType); this.sub = @@ -239,14 +227,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { .organizations$(userId) .pipe(getOrganizationById(this.organizationId)), ); - if (this.deprecateStripeSourcesAPI) { - const { accountCredit, paymentSource } = - await this.billingApiService.getOrganizationPaymentMethod(this.organizationId); - this.accountCredit = accountCredit; - this.paymentSource = paymentSource; - } else { - this.billing = await this.organizationApiService.getBilling(this.organizationId); - } + const { accountCredit, paymentSource } = + await this.billingApiService.getOrganizationPaymentMethod(this.organizationId); + this.accountCredit = accountCredit; + this.paymentSource = paymentSource; } if (!this.selfHosted) { @@ -333,16 +317,8 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return this.selectableProducts.find((product) => product.productTier === productTier); } - secretsManagerTrialDiscount() { - return this.sub?.customerDiscount?.appliesTo?.includes("sm-standalone") - ? this.discountPercentage - : this.discountPercentageFromSub + this.discountPercentage; - } - isPaymentSourceEmpty() { - return this.deprecateStripeSourcesAPI - ? this.paymentSource === null || this.paymentSource === undefined - : this.billing?.paymentSource === null || this.billing?.paymentSource === undefined; + return this.paymentSource === null || this.paymentSource === undefined; } isSecretsManagerTrial(): boolean { @@ -486,9 +462,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { get upgradeRequiresPaymentMethod() { const isFreeTier = this.organization?.productTierType === ProductTierType.Free; const shouldHideFree = !this.showFree; - const hasNoPaymentSource = this.deprecateStripeSourcesAPI - ? !this.paymentSource - : !this.billing?.paymentSource; + const hasNoPaymentSource = !this.paymentSource; return isFreeTier && shouldHideFree && hasNoPaymentSource; } @@ -721,25 +695,13 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } changedCountry() { - if (this.deprecateStripeSourcesAPI && this.paymentV2Component) { - this.paymentV2Component.showBankAccount = this.taxInformation.country === "US"; + this.paymentComponent.showBankAccount = this.taxInformation.country === "US"; - if ( - !this.paymentV2Component.showBankAccount && - this.paymentV2Component.selected === PaymentMethodType.BankAccount - ) { - this.paymentV2Component.select(PaymentMethodType.Card); - } - } else if (this.paymentComponent && this.taxInformation) { - this.paymentComponent!.hideBank = this.taxInformation.country !== "US"; - // Bank Account payments are only available for US customers - if ( - this.paymentComponent.hideBank && - this.paymentComponent.method === PaymentMethodType.BankAccount - ) { - this.paymentComponent.method = PaymentMethodType.Card; - this.paymentComponent.changeMethod(); - } + if ( + !this.paymentComponent.showBankAccount && + this.paymentComponent.selected === PaymentMethodType.BankAccount + ) { + this.paymentComponent.select(PaymentMethodType.Card); } } @@ -821,14 +783,8 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { plan.secretsManagerSeats = org.smSeats; } - let paymentMethod: [string, PaymentMethodType]; - - if (this.deprecateStripeSourcesAPI) { - const { type, token } = await this.paymentV2Component.tokenize(); - paymentMethod = [token, type]; - } else { - paymentMethod = await this.paymentComponent.createPaymentToken(); - } + const { type, token } = await this.paymentComponent.tokenize(); + const paymentMethod: [string, PaymentMethodType] = [token, type]; const payment: PaymentInformation = { paymentMethod, @@ -864,27 +820,17 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.buildSecretsManagerRequest(request); if (this.upgradeRequiresPaymentMethod || this.showPayment || this.isPaymentSourceEmpty()) { - if (this.deprecateStripeSourcesAPI) { - const tokenizedPaymentSource = await this.paymentV2Component.tokenize(); - const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); - updatePaymentMethodRequest.paymentSource = tokenizedPaymentSource; - updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From( - this.taxInformation, - ); - - await this.billingApiService.updateOrganizationPaymentMethod( - this.organizationId, - updatePaymentMethodRequest, - ); - } else { - const tokenResult = await this.paymentComponent.createPaymentToken(); - const paymentRequest = new PaymentRequest(); - paymentRequest.paymentToken = tokenResult[0]; - paymentRequest.paymentMethodType = tokenResult[1]; - paymentRequest.country = this.taxInformation.country; - paymentRequest.postalCode = this.taxInformation.postalCode; - await this.organizationApiService.updatePayment(this.organizationId, paymentRequest); - } + const tokenizedPaymentSource = await this.paymentComponent.tokenize(); + const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); + updatePaymentMethodRequest.paymentSource = tokenizedPaymentSource; + updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From( + this.taxInformation, + ); + + await this.billingApiService.updateOrganizationPaymentMethod( + this.organizationId, + updatePaymentMethodRequest, + ); } // Backfill pub/priv key if necessary @@ -894,10 +840,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); } - const result = await this.organizationApiService.upgrade(this.organizationId, request); - if (!result.success && result.paymentIntentClientSecret != null) { - await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null); - } + await this.organizationApiService.upgrade(this.organizationId, request); return this.organizationId; } @@ -994,38 +937,20 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } get paymentSourceClasses() { - if (this.deprecateStripeSourcesAPI) { - if (this.paymentSource == null) { - return []; - } - switch (this.paymentSource.type) { - case PaymentMethodType.Card: - return ["bwi-credit-card"]; - case PaymentMethodType.BankAccount: - return ["bwi-bank"]; - case PaymentMethodType.Check: - return ["bwi-money"]; - case PaymentMethodType.PayPal: - return ["bwi-paypal text-primary"]; - default: - return []; - } - } else { - if (this.billing.paymentSource == null) { + if (this.paymentSource == null) { + return []; + } + switch (this.paymentSource.type) { + case PaymentMethodType.Card: + return ["bwi-credit-card"]; + case PaymentMethodType.BankAccount: + return ["bwi-bank"]; + case PaymentMethodType.Check: + return ["bwi-money"]; + case PaymentMethodType.PayPal: + return ["bwi-paypal text-primary"]; + default: return []; - } - switch (this.billing.paymentSource.type) { - case PaymentMethodType.Card: - return ["bwi-credit-card"]; - case PaymentMethodType.BankAccount: - return ["bwi-bank"]; - case PaymentMethodType.Check: - return ["bwi-money"]; - case PaymentMethodType.PayPal: - return ["bwi-paypal text-primary"]; - default: - return []; - } } } diff --git a/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts b/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts index 3d4c8dd3870..1bfb9fc4912 100644 --- a/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts @@ -1,14 +1,11 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; -import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; import { canAccessBillingTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { organizationPermissionsGuard } from "../../admin-console/organizations/guards/org-permissions.guard"; import { organizationIsUnmanaged } from "../../billing/guards/organization-is-unmanaged.guard"; import { WebPlatformUtilsService } from "../../core/web-platform-utils.service"; -import { PaymentMethodComponent } from "../shared"; import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component"; import { OrganizationSubscriptionCloudComponent } from "./organization-subscription-cloud.component"; @@ -28,21 +25,17 @@ const routes: Routes = [ : OrganizationSubscriptionCloudComponent, data: { titleId: "subscription" }, }, - ...featureFlaggedRoute({ - defaultComponent: PaymentMethodComponent, - flaggedComponent: OrganizationPaymentMethodComponent, - featureFlag: FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - routeOptions: { - path: "payment-method", - canActivate: [ - organizationPermissionsGuard((org) => org.canEditPaymentMethods), - organizationIsUnmanaged, - ], - data: { - titleId: "paymentMethod", - }, + { + path: "payment-method", + component: OrganizationPaymentMethodComponent, + canActivate: [ + organizationPermissionsGuard((org) => org.canEditPaymentMethods), + organizationIsUnmanaged, + ], + data: { + titleId: "paymentMethod", }, - }), + }, { path: "history", component: OrgBillingHistoryViewComponent, diff --git a/apps/web/src/app/billing/organizations/organization-billing.module.ts b/apps/web/src/app/billing/organizations/organization-billing.module.ts index 48ac613711d..d8f4b7393aa 100644 --- a/apps/web/src/app/billing/organizations/organization-billing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing.module.ts @@ -1,5 +1,7 @@ import { NgModule } from "@angular/core"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { BannerModule } from "../../../../../../libs/components/src/banner/banner.module"; import { UserVerificationModule } from "../../auth/shared/components/user-verification"; import { LooseComponentsModule } from "../../shared"; diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.html b/apps/web/src/app/billing/organizations/organization-plans.component.html index d37f95e3aa2..2566250c823 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.html +++ b/apps/web/src/app/billing/organizations/organization-plans.component.html @@ -433,13 +433,7 @@

{{ paymentDesc }}

- - + }}

- - - diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index 0b09ac2c6de..f1e090ef50d 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -36,7 +36,6 @@ import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/ta import { PaymentMethodType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; import { TaxInformation } from "@bitwarden/common/billing/models/domain"; import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; -import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request"; import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request"; import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request"; import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response"; @@ -57,7 +56,6 @@ import { KeyService } from "@bitwarden/key-management"; import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module"; import { BillingSharedModule, secretsManagerSubscribeFormFactory } from "../shared"; -import { PaymentV2Component } from "../shared/payment/payment-v2.component"; import { PaymentComponent } from "../shared/payment/payment.component"; interface OnSuccessArgs { @@ -79,7 +77,6 @@ const Allowed2020PlansForLegacyProviders = [ }) export class OrganizationPlansComponent implements OnInit, OnDestroy { @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; - @ViewChild(PaymentV2Component) paymentV2Component: PaymentV2Component; @ViewChild(ManageTaxInformationComponent) taxComponent: ManageTaxInformationComponent; @Input() organizationId?: string; @@ -128,7 +125,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { singleOrgPolicyAppliesToActiveUser = false; isInTrialFlow = false; discount = 0; - deprecateStripeSourcesAPI: boolean; protected useLicenseUploaderComponent$ = this.configService.getFeatureFlag$( FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader, @@ -189,10 +185,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } async ngOnInit() { - this.deprecateStripeSourcesAPI = await this.configService.getFeatureFlag( - FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - ); - if (this.organizationId) { const userId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), @@ -580,23 +572,12 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } protected changedCountry(): void { - if (this.deprecateStripeSourcesAPI) { - this.paymentV2Component.showBankAccount = this.taxInformation?.country === "US"; - if ( - !this.paymentV2Component.showBankAccount && - this.paymentV2Component.selected === PaymentMethodType.BankAccount - ) { - this.paymentV2Component.select(PaymentMethodType.Card); - } - } else { - this.paymentComponent.hideBank = this.taxInformation?.country !== "US"; - if ( - this.paymentComponent.hideBank && - this.paymentComponent.method === PaymentMethodType.BankAccount - ) { - this.paymentComponent.method = PaymentMethodType.Card; - this.paymentComponent.changeMethod(); - } + this.paymentComponent.showBankAccount = this.taxInformation?.country === "US"; + if ( + !this.paymentComponent.showBankAccount && + this.paymentComponent.selected === PaymentMethodType.BankAccount + ) { + this.paymentComponent.select(PaymentMethodType.Card); } } @@ -751,25 +732,15 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.buildSecretsManagerRequest(request); if (this.upgradeRequiresPaymentMethod) { - if (this.deprecateStripeSourcesAPI) { - const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); - updatePaymentMethodRequest.paymentSource = await this.paymentV2Component.tokenize(); - updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From( - this.taxInformation, - ); - await this.billingApiService.updateOrganizationPaymentMethod( - this.organizationId, - updatePaymentMethodRequest, - ); - } else { - const [paymentToken, paymentMethodType] = await this.paymentComponent.createPaymentToken(); - const paymentRequest = new PaymentRequest(); - paymentRequest.paymentToken = paymentToken; - paymentRequest.paymentMethodType = paymentMethodType; - paymentRequest.country = this.taxInformation?.country; - paymentRequest.postalCode = this.taxInformation?.postalCode; - await this.organizationApiService.updatePayment(this.organizationId, paymentRequest); - } + const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); + updatePaymentMethodRequest.paymentSource = await this.paymentComponent.tokenize(); + updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From( + this.taxInformation, + ); + await this.billingApiService.updateOrganizationPaymentMethod( + this.organizationId, + updatePaymentMethodRequest, + ); } // Backfill pub/priv key if necessary @@ -779,10 +750,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); } - const result = await this.organizationApiService.upgrade(this.organizationId, request); - if (!result.success && result.paymentIntentClientSecret != null) { - await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null); - } + await this.organizationApiService.upgrade(this.organizationId, request); return this.organizationId; } @@ -803,14 +771,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { if (this.selectedPlan.type === PlanType.Free) { request.planType = PlanType.Free; } else { - let type: PaymentMethodType; - let token: string; - - if (this.deprecateStripeSourcesAPI) { - ({ type, token } = await this.paymentV2Component.tokenize()); - } else { - [token, type] = await this.paymentComponent.createPaymentToken(); - } + const { type, token } = await this.paymentComponent.tokenize(); request.paymentToken = token; request.paymentMethodType = type; diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index 7f81a1fe230..003f816ac30 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -25,12 +25,8 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { DialogService, ToastService } from "@bitwarden/components"; import { - AdjustStorageDialogV2Component, - AdjustStorageDialogV2ResultType, -} from "../shared/adjust-storage-dialog/adjust-storage-dialog-v2.component"; -import { - AdjustStorageDialogResult, - openAdjustStorageDialog, + AdjustStorageDialogComponent, + AdjustStorageDialogResultType, } from "../shared/adjust-storage-dialog/adjust-storage-dialog.component"; import { OffboardingSurveyDialogResultType, @@ -55,7 +51,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy organizationId: string; userOrg: Organization; showChangePlan = false; - showDownloadLicense = false; hasBillingSyncToken: boolean; showAdjustSecretsManager = false; showSecretsManagerSubscribe = false; @@ -70,10 +65,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy protected readonly subscriptionHiddenIcon = SubscriptionHiddenIcon; protected readonly teamsStarter = ProductTierType.TeamsStarter; - protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$( - FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - ); - private destroy$ = new Subject(); constructor( @@ -426,36 +417,19 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy adjustStorage = (add: boolean) => { return async () => { - const deprecateStripeSourcesAPI = await firstValueFrom(this.deprecateStripeSourcesAPI$); - - if (deprecateStripeSourcesAPI) { - const dialogRef = AdjustStorageDialogV2Component.open(this.dialogService, { - data: { - price: this.storageGbPrice, - cadence: this.billingInterval, - type: add ? "Add" : "Remove", - organizationId: this.organizationId, - }, - }); - - const result = await lastValueFrom(dialogRef.closed); - - if (result === AdjustStorageDialogV2ResultType.Submitted) { - await this.load(); - } - } else { - const dialogRef = openAdjustStorageDialog(this.dialogService, { - data: { - storageGbPrice: this.storageGbPrice, - add: add, - organizationId: this.organizationId, - interval: this.billingInterval, - }, - }); - const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustStorageDialogResult.Adjusted) { - await this.load(); - } + const dialogRef = AdjustStorageDialogComponent.open(this.dialogService, { + data: { + price: this.storageGbPrice, + cadence: this.billingInterval, + type: add ? "Add" : "Remove", + organizationId: this.organizationId, + }, + }); + + const result = await lastValueFrom(dialogRef.closed); + + if (result === AdjustStorageDialogResultType.Submitted) { + await this.load(); } }; }; diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts index a5b18d9edbf..3fb2121b036 100644 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts +++ b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts @@ -30,9 +30,9 @@ import { openAddCreditDialog, } from "../../shared/add-credit-dialog.component"; import { - AdjustPaymentDialogV2Component, - AdjustPaymentDialogV2ResultType, -} from "../../shared/adjust-payment-dialog/adjust-payment-dialog-v2.component"; + AdjustPaymentDialogComponent, + AdjustPaymentDialogResultType, +} from "../../shared/adjust-payment-dialog/adjust-payment-dialog.component"; @Component({ templateUrl: "./organization-payment-method.component.html", @@ -159,7 +159,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { }; protected updatePaymentMethod = async (): Promise => { - const dialogRef = AdjustPaymentDialogV2Component.open(this.dialogService, { + const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, { data: { initialPaymentMethod: this.paymentSource?.type, organizationId: this.organizationId, @@ -169,13 +169,13 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustPaymentDialogV2ResultType.Submitted) { + if (result === AdjustPaymentDialogResultType.Submitted) { await this.load(); } }; changePayment = async () => { - const dialogRef = AdjustPaymentDialogV2Component.open(this.dialogService, { + const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, { data: { initialPaymentMethod: this.paymentSource?.type, organizationId: this.organizationId, @@ -183,7 +183,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { }, }); const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustPaymentDialogV2ResultType.Submitted) { + if (result === AdjustPaymentDialogResultType.Submitted) { this.location.replaceState(this.location.path(), "", {}); if (this.launchPaymentModalAutomatically && !this.organization.enabled) { await this.syncService.fullSync(true); diff --git a/apps/web/src/app/billing/services/free-families-policy.service.ts b/apps/web/src/app/billing/services/free-families-policy.service.ts index c148bd007be..b07ccefdbab 100644 --- a/apps/web/src/app/billing/services/free-families-policy.service.ts +++ b/apps/web/src/app/billing/services/free-families-policy.service.ts @@ -6,6 +6,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -110,7 +111,11 @@ export class FreeFamiliesPolicyService { }); } - return this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy).pipe( + return this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId), + ), map((policies) => ({ isFreeFamilyPolicyEnabled: policies.some( (policy) => policy.organizationId === organizationId && policy.enabled, diff --git a/apps/web/src/app/billing/settings/sponsored-families.component.ts b/apps/web/src/app/billing/settings/sponsored-families.component.ts index b9f70dd3085..d4a93ba7cd8 100644 --- a/apps/web/src/app/billing/settings/sponsored-families.component.ts +++ b/apps/web/src/app/billing/settings/sponsored-families.component.ts @@ -98,7 +98,7 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy { this.availableSponsorshipOrgs$ = combineLatest([ this.organizationService.organizations$(userId), - this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy), + this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId), ]).pipe( map(([organizations, policies]) => organizations diff --git a/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts b/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts index b40902112c8..6e9e00b0ee1 100644 --- a/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts +++ b/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts @@ -2,12 +2,14 @@ // @ts-strict-ignore import { formatDate } from "@angular/common"; import { Component, EventEmitter, Input, Output, OnInit } from "@angular/core"; -import { firstValueFrom, map, Observable } from "rxjs"; +import { firstValueFrom, map, Observable, switchMap } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; 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 { 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"; @@ -38,6 +40,7 @@ export class SponsoringOrgRowComponent implements OnInit { private toastService: ToastService, private configService: ConfigService, private policyService: PolicyService, + private accountService: AccountService, ) {} async ngOnInit() { @@ -54,17 +57,19 @@ export class SponsoringOrgRowComponent implements OnInit { ); if (this.isFreeFamilyFlagEnabled) { - this.isFreeFamilyPolicyEnabled$ = this.policyService - .getAll$(PolicyType.FreeFamiliesSponsorshipPolicy) - .pipe( - map( - (policies) => - Array.isArray(policies) && - policies.some( - (policy) => policy.organizationId === this.sponsoringOrg.id && policy.enabled, - ), - ), - ); + this.isFreeFamilyPolicyEnabled$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId), + ), + map( + (policies) => + Array.isArray(policies) && + policies.some( + (policy) => policy.organizationId === this.sponsoringOrg.id && policy.enabled, + ), + ), + ); } } diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html deleted file mode 100644 index bb06f87ca03..00000000000 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.ts deleted file mode 100644 index 0a72b8302bc..00000000000 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.ts +++ /dev/null @@ -1,177 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, forwardRef, Inject, OnInit, ViewChild } from "@angular/core"; - -import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; -import { PaymentMethodType, ProductTierType } from "@bitwarden/common/billing/enums"; -import { TaxInformation } from "@bitwarden/common/billing/models/domain"; -import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; -import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request"; -import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request"; -import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { DialogService, ToastService } from "@bitwarden/components"; - -import { PaymentV2Component } from "../payment/payment-v2.component"; - -export interface AdjustPaymentDialogV2Params { - initialPaymentMethod?: PaymentMethodType; - organizationId?: string; - productTier?: ProductTierType; -} - -export enum AdjustPaymentDialogV2ResultType { - Closed = "closed", - Submitted = "submitted", -} - -@Component({ - templateUrl: "./adjust-payment-dialog-v2.component.html", -}) -export class AdjustPaymentDialogV2Component implements OnInit { - @ViewChild(PaymentV2Component) paymentComponent: PaymentV2Component; - @ViewChild(forwardRef(() => ManageTaxInformationComponent)) - taxInfoComponent: ManageTaxInformationComponent; - - protected readonly PaymentMethodType = PaymentMethodType; - protected readonly ResultType = AdjustPaymentDialogV2ResultType; - - protected dialogHeader: string; - protected initialPaymentMethod: PaymentMethodType; - protected organizationId?: string; - protected productTier?: ProductTierType; - - protected taxInformation: TaxInformation; - - constructor( - private apiService: ApiService, - private billingApiService: BillingApiServiceAbstraction, - private organizationApiService: OrganizationApiServiceAbstraction, - @Inject(DIALOG_DATA) protected dialogParams: AdjustPaymentDialogV2Params, - private dialogRef: DialogRef, - private i18nService: I18nService, - private toastService: ToastService, - ) { - const key = this.dialogParams.initialPaymentMethod ? "changePaymentMethod" : "addPaymentMethod"; - this.dialogHeader = this.i18nService.t(key); - this.initialPaymentMethod = this.dialogParams.initialPaymentMethod ?? PaymentMethodType.Card; - this.organizationId = this.dialogParams.organizationId; - this.productTier = this.dialogParams.productTier; - } - - ngOnInit(): void { - if (this.organizationId) { - this.organizationApiService - .getTaxInfo(this.organizationId) - .then((response: TaxInfoResponse) => { - this.taxInformation = TaxInformation.from(response); - }) - .catch(() => { - this.taxInformation = new TaxInformation(); - }); - } else { - this.apiService - .getTaxInfo() - .then((response: TaxInfoResponse) => { - this.taxInformation = TaxInformation.from(response); - }) - .catch(() => { - this.taxInformation = new TaxInformation(); - }); - } - } - - taxInformationChanged(event: TaxInformation) { - this.taxInformation = event; - if (event.country === "US") { - this.paymentComponent.showBankAccount = !!this.organizationId; - } else { - this.paymentComponent.showBankAccount = false; - if (this.paymentComponent.selected === PaymentMethodType.BankAccount) { - this.paymentComponent.select(PaymentMethodType.Card); - } - } - } - - submit = async (): Promise => { - if (!this.taxInfoComponent.validate()) { - return; - } - - try { - if (!this.organizationId) { - await this.updatePremiumUserPaymentMethod(); - } else { - await this.updateOrganizationPaymentMethod(); - } - - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("updatedPaymentMethod"), - }); - - this.dialogRef.close(AdjustPaymentDialogV2ResultType.Submitted); - } catch (error) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t(error.message) || error.message, - }); - } - }; - - private updateOrganizationPaymentMethod = async () => { - const paymentSource = await this.paymentComponent.tokenize(); - - const request = new UpdatePaymentMethodRequest(); - request.paymentSource = paymentSource; - request.taxInformation = ExpandedTaxInfoUpdateRequest.From(this.taxInformation); - - await this.billingApiService.updateOrganizationPaymentMethod(this.organizationId, request); - }; - - protected get showTaxIdField(): boolean { - if (!this.organizationId) { - return false; - } - - switch (this.productTier) { - case ProductTierType.Free: - case ProductTierType.Families: - return false; - default: - return true; - } - } - - private updatePremiumUserPaymentMethod = async () => { - const { type, token } = await this.paymentComponent.tokenize(); - - const request = new PaymentRequest(); - request.paymentMethodType = type; - request.paymentToken = token; - request.country = this.taxInformation.country; - request.postalCode = this.taxInformation.postalCode; - request.taxId = this.taxInformation.taxId; - request.state = this.taxInformation.state; - request.line1 = this.taxInformation.line1; - request.line2 = this.taxInformation.line2; - request.city = this.taxInformation.city; - request.state = this.taxInformation.state; - await this.apiService.postAccountPayment(request); - }; - - static open = ( - dialogService: DialogService, - dialogConfig: DialogConfig, - ) => - dialogService.open( - AdjustPaymentDialogV2Component, - dialogConfig, - ); -} diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html index de607314354..4f7990f11a3 100644 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html @@ -1,30 +1,29 @@ - - - - - - - - - - - - + + + + + + + + + + diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts index bbae5099afa..0fc49b2ddc1 100644 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts @@ -1,59 +1,66 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, Inject, OnInit, ViewChild } from "@angular/core"; -import { FormGroup } from "@angular/forms"; +import { Component, forwardRef, Inject, OnInit, ViewChild } from "@angular/core"; import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; +import { PaymentMethodType, ProductTierType } from "@bitwarden/common/billing/enums"; import { TaxInformation } from "@bitwarden/common/billing/models/domain"; +import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request"; +import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request"; import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DialogService, ToastService } from "@bitwarden/components"; import { PaymentComponent } from "../payment/payment.component"; -export interface AdjustPaymentDialogData { - organizationId: string; - currentType: PaymentMethodType; +export interface AdjustPaymentDialogParams { + initialPaymentMethod?: PaymentMethodType; + organizationId?: string; + productTier?: ProductTierType; } -export enum AdjustPaymentDialogResult { - Adjusted = "adjusted", - Cancelled = "cancelled", +export enum AdjustPaymentDialogResultType { + Closed = "closed", + Submitted = "submitted", } @Component({ - templateUrl: "adjust-payment-dialog.component.html", + templateUrl: "./adjust-payment-dialog.component.html", }) export class AdjustPaymentDialogComponent implements OnInit { - @ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent; - @ViewChild(ManageTaxInformationComponent) taxInfoComponent: ManageTaxInformationComponent; + @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; + @ViewChild(forwardRef(() => ManageTaxInformationComponent)) + taxInfoComponent: ManageTaxInformationComponent; - organizationId: string; - currentType: PaymentMethodType; - paymentMethodType = PaymentMethodType; + protected readonly PaymentMethodType = PaymentMethodType; + protected readonly ResultType = AdjustPaymentDialogResultType; - protected DialogResult = AdjustPaymentDialogResult; - protected formGroup = new FormGroup({}); + protected dialogHeader: string; + protected initialPaymentMethod: PaymentMethodType; + protected organizationId?: string; + protected productTier?: ProductTierType; protected taxInformation: TaxInformation; constructor( - private dialogRef: DialogRef, - @Inject(DIALOG_DATA) protected data: AdjustPaymentDialogData, private apiService: ApiService, - private i18nService: I18nService, + private billingApiService: BillingApiServiceAbstraction, private organizationApiService: OrganizationApiServiceAbstraction, - private configService: ConfigService, + @Inject(DIALOG_DATA) protected dialogParams: AdjustPaymentDialogParams, + private dialogRef: DialogRef, + private i18nService: I18nService, private toastService: ToastService, ) { - this.organizationId = data.organizationId; - this.currentType = data.currentType; + const key = this.dialogParams.initialPaymentMethod ? "changePaymentMethod" : "addPaymentMethod"; + this.dialogHeader = this.i18nService.t(key); + this.initialPaymentMethod = this.dialogParams.initialPaymentMethod ?? PaymentMethodType.Card; + this.organizationId = this.dialogParams.organizationId; + this.productTier = this.dialogParams.productTier; } ngOnInit(): void { @@ -78,65 +85,92 @@ export class AdjustPaymentDialogComponent implements OnInit { } } - submit = async () => { - if (!this.taxInfoComponent?.validate()) { - return; - } - - const request = new PaymentRequest(); - const response = this.paymentComponent.createPaymentToken().then((result) => { - request.paymentToken = result[0]; - request.paymentMethodType = result[1]; - request.postalCode = this.taxInformation?.postalCode; - request.country = this.taxInformation?.country; - request.taxId = this.taxInformation?.taxId; - if (this.organizationId == null) { - return this.apiService.postAccountPayment(request); - } else { - request.taxId = this.taxInformation?.taxId; - request.state = this.taxInformation?.state; - request.line1 = this.taxInformation?.line1; - request.line2 = this.taxInformation?.line2; - request.city = this.taxInformation?.city; - request.state = this.taxInformation?.state; - return this.organizationApiService.updatePayment(this.organizationId, request); - } - }); - await response; - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("updatedPaymentMethod"), - }); - this.dialogRef.close(AdjustPaymentDialogResult.Adjusted); - }; - taxInformationChanged(event: TaxInformation) { this.taxInformation = event; if (event.country === "US") { - this.paymentComponent.hideBank = !this.organizationId; + this.paymentComponent.showBankAccount = !!this.organizationId; } else { - this.paymentComponent.hideBank = true; - if (this.paymentComponent.method === PaymentMethodType.BankAccount) { - this.paymentComponent.method = PaymentMethodType.Card; - this.paymentComponent.changeMethod(); + this.paymentComponent.showBankAccount = false; + if (this.paymentComponent.selected === PaymentMethodType.BankAccount) { + this.paymentComponent.select(PaymentMethodType.Card); } } } + submit = async (): Promise => { + if (!this.taxInfoComponent.validate()) { + this.taxInfoComponent.markAllAsTouched(); + return; + } + + try { + if (!this.organizationId) { + await this.updatePremiumUserPaymentMethod(); + } else { + await this.updateOrganizationPaymentMethod(); + } + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("updatedPaymentMethod"), + }); + + this.dialogRef.close(AdjustPaymentDialogResultType.Submitted); + } catch (error) { + const msg = typeof error == "object" ? error.message : error; + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t(msg) || msg, + }); + } + }; + + private updateOrganizationPaymentMethod = async () => { + const paymentSource = await this.paymentComponent.tokenize(); + + const request = new UpdatePaymentMethodRequest(); + request.paymentSource = paymentSource; + request.taxInformation = ExpandedTaxInfoUpdateRequest.From(this.taxInformation); + + await this.billingApiService.updateOrganizationPaymentMethod(this.organizationId, request); + }; + protected get showTaxIdField(): boolean { - return !!this.organizationId; + if (!this.organizationId) { + return false; + } + + switch (this.productTier) { + case ProductTierType.Free: + case ProductTierType.Families: + return false; + default: + return true; + } } -} -/** - * Strongly typed helper to open a AdjustPaymentDialog - * @param dialogService Instance of the dialog service that will be used to open the dialog - * @param config Configuration for the dialog - */ -export function openAdjustPaymentDialog( - dialogService: DialogService, - config: DialogConfig, -) { - return dialogService.open(AdjustPaymentDialogComponent, config); + private updatePremiumUserPaymentMethod = async () => { + const { type, token } = await this.paymentComponent.tokenize(); + + const request = new PaymentRequest(); + request.paymentMethodType = type; + request.paymentToken = token; + request.country = this.taxInformation.country; + request.postalCode = this.taxInformation.postalCode; + request.taxId = this.taxInformation.taxId; + request.state = this.taxInformation.state; + request.line1 = this.taxInformation.line1; + request.line2 = this.taxInformation.line2; + request.city = this.taxInformation.city; + request.state = this.taxInformation.state; + await this.apiService.postAccountPayment(request); + }; + + static open = ( + dialogService: DialogService, + dialogConfig: DialogConfig, + ) => + dialogService.open(AdjustPaymentDialogComponent, dialogConfig); } diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.html b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.html deleted file mode 100644 index 7b74379acb6..00000000000 --- a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.html +++ /dev/null @@ -1,34 +0,0 @@ -
- - -

{{ body }}

-
- - {{ storageFieldLabel }} - - - - {{ "total" | i18n }} - {{ this.formGroup.value.storage }} GB × {{ this.price | currency: "$" }} = - {{ this.price * this.formGroup.value.storage | currency: "$" }} / - {{ this.cadence | i18n }} - - -
-
- - - - -
-
diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.ts b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.ts deleted file mode 100644 index ba7619729bf..00000000000 --- a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.ts +++ /dev/null @@ -1,106 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, Inject } from "@angular/core"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { StorageRequest } from "@bitwarden/common/models/request/storage.request"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { DialogService, ToastService } from "@bitwarden/components"; - -export interface AdjustStorageDialogV2Params { - price: number; - cadence: "month" | "year"; - type: "Add" | "Remove"; - organizationId?: string; -} - -export enum AdjustStorageDialogV2ResultType { - Submitted = "submitted", - Closed = "closed", -} - -@Component({ - templateUrl: "./adjust-storage-dialog-v2.component.html", -}) -export class AdjustStorageDialogV2Component { - protected formGroup = new FormGroup({ - storage: new FormControl(0, [ - Validators.required, - Validators.min(0), - Validators.max(99), - ]), - }); - - protected organizationId?: string; - protected price: number; - protected cadence: "month" | "year"; - - protected title: string; - protected body: string; - protected storageFieldLabel: string; - - protected ResultType = AdjustStorageDialogV2ResultType; - - constructor( - private apiService: ApiService, - @Inject(DIALOG_DATA) protected dialogParams: AdjustStorageDialogV2Params, - private dialogRef: DialogRef, - private i18nService: I18nService, - private organizationApiService: OrganizationApiServiceAbstraction, - private toastService: ToastService, - ) { - this.price = this.dialogParams.price; - this.cadence = this.dialogParams.cadence; - this.organizationId = this.dialogParams.organizationId; - switch (this.dialogParams.type) { - case "Add": - this.title = this.i18nService.t("addStorage"); - this.body = this.i18nService.t("storageAddNote"); - this.storageFieldLabel = this.i18nService.t("gbStorageAdd"); - break; - case "Remove": - this.title = this.i18nService.t("removeStorage"); - this.body = this.i18nService.t("storageRemoveNote"); - this.storageFieldLabel = this.i18nService.t("gbStorageRemove"); - break; - } - } - - submit = async () => { - const request = new StorageRequest(); - switch (this.dialogParams.type) { - case "Add": - request.storageGbAdjustment = this.formGroup.value.storage; - break; - case "Remove": - request.storageGbAdjustment = this.formGroup.value.storage * -1; - break; - } - - if (this.organizationId) { - await this.organizationApiService.updateStorage(this.organizationId, request); - } else { - await this.apiService.postAccountStorage(request); - } - - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), - }); - - this.dialogRef.close(this.ResultType.Submitted); - }; - - static open = ( - dialogService: DialogService, - dialogConfig: DialogConfig, - ) => - dialogService.open( - AdjustStorageDialogV2Component, - dialogConfig, - ); -} diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.html b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.html index a597a3ae5ea..832356477c4 100644 --- a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.html +++ b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.html @@ -1,17 +1,17 @@
- + -

{{ (add ? "storageAddNote" : "storageRemoveNote") | i18n }}

+

{{ body }}

- {{ (add ? "gbStorageAdd" : "gbStorageRemove") | i18n }} - - - {{ "total" | i18n }}: - {{ formGroup.get("storageAdjustment").value || 0 }} GB × - {{ storageGbPrice | currency: "$" }} = {{ adjustedStorageTotal | currency: "$" }} /{{ - interval | i18n - }} + {{ storageFieldLabel }} + + + + {{ "total" | i18n }} + {{ this.formGroup.value.storage }} GB × {{ this.price | currency: "$" }} = + {{ this.price * this.formGroup.value.storage | currency: "$" }} / + {{ this.cadence | i18n }}
@@ -25,11 +25,10 @@ bitButton bitFormButton buttonType="secondary" - [bitDialogClose]="DialogResult.Cancelled" + [bitDialogClose]="ResultType.Closed" > {{ "cancel" | i18n }}
- diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts index f69f9e3eaad..4362e36f857 100644 --- a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts +++ b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts @@ -1,132 +1,103 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, Inject, ViewChild } from "@angular/core"; +import { Component, Inject } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { PaymentResponse } from "@bitwarden/common/billing/models/response/payment.response"; import { StorageRequest } from "@bitwarden/common/models/request/storage.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService, ToastService } from "@bitwarden/components"; -import { PaymentComponent } from "../payment/payment.component"; - -export interface AdjustStorageDialogData { - storageGbPrice: number; - add: boolean; +export interface AdjustStorageDialogParams { + price: number; + cadence: "month" | "year"; + type: "Add" | "Remove"; organizationId?: string; - interval?: string; } -export enum AdjustStorageDialogResult { - Adjusted = "adjusted", - Cancelled = "cancelled", +export enum AdjustStorageDialogResultType { + Submitted = "submitted", + Closed = "closed", } @Component({ - templateUrl: "adjust-storage-dialog.component.html", + templateUrl: "./adjust-storage-dialog.component.html", }) export class AdjustStorageDialogComponent { - storageGbPrice: number; - add: boolean; - organizationId: string; - interval: string; - - @ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent; - - protected DialogResult = AdjustStorageDialogResult; protected formGroup = new FormGroup({ - storageAdjustment: new FormControl(0, [ + storage: new FormControl(0, [ Validators.required, Validators.min(0), Validators.max(99), ]), }); + protected organizationId?: string; + protected price: number; + protected cadence: "month" | "year"; + + protected title: string; + protected body: string; + protected storageFieldLabel: string; + + protected ResultType = AdjustStorageDialogResultType; + constructor( - private dialogRef: DialogRef, - @Inject(DIALOG_DATA) protected data: AdjustStorageDialogData, private apiService: ApiService, + @Inject(DIALOG_DATA) protected dialogParams: AdjustStorageDialogParams, + private dialogRef: DialogRef, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private router: Router, - private activatedRoute: ActivatedRoute, - private logService: LogService, private organizationApiService: OrganizationApiServiceAbstraction, private toastService: ToastService, ) { - this.storageGbPrice = data.storageGbPrice; - this.add = data.add; - this.organizationId = data.organizationId; - this.interval = data.interval || "year"; + this.price = this.dialogParams.price; + this.cadence = this.dialogParams.cadence; + this.organizationId = this.dialogParams.organizationId; + switch (this.dialogParams.type) { + case "Add": + this.title = this.i18nService.t("addStorage"); + this.body = this.i18nService.t("storageAddNote"); + this.storageFieldLabel = this.i18nService.t("gbStorageAdd"); + break; + case "Remove": + this.title = this.i18nService.t("removeStorage"); + this.body = this.i18nService.t("storageRemoveNote"); + this.storageFieldLabel = this.i18nService.t("gbStorageRemove"); + break; + } } submit = async () => { const request = new StorageRequest(); - request.storageGbAdjustment = this.formGroup.value.storageAdjustment; - if (!this.add) { - request.storageGbAdjustment *= -1; + switch (this.dialogParams.type) { + case "Add": + request.storageGbAdjustment = this.formGroup.value.storage; + break; + case "Remove": + request.storageGbAdjustment = this.formGroup.value.storage * -1; + break; } - let paymentFailed = false; - const action = async () => { - let response: Promise; - if (this.organizationId == null) { - response = this.apiService.postAccountStorage(request); - } else { - response = this.organizationApiService.updateStorage(this.organizationId, request); - } - const result = await response; - if (result != null && result.paymentIntentClientSecret != null) { - try { - await this.paymentComponent.handleStripeCardPayment( - result.paymentIntentClientSecret, - null, - ); - } catch { - paymentFailed = true; - } - } - }; - await action(); - this.dialogRef.close(AdjustStorageDialogResult.Adjusted); - if (paymentFailed) { - this.toastService.showToast({ - variant: "warning", - title: null, - message: this.i18nService.t("couldNotChargeCardPayInvoice"), - timeout: 10000, - }); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["../billing"], { relativeTo: this.activatedRoute }); + if (this.organizationId) { + await this.organizationApiService.updateStorage(this.organizationId, request); } else { - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), - }); + await this.apiService.postAccountStorage(request); } - }; - get adjustedStorageTotal(): number { - return this.storageGbPrice * this.formGroup.value.storageAdjustment; - } -} + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), + }); + + this.dialogRef.close(this.ResultType.Submitted); + }; -/** - * Strongly typed helper to open an AdjustStorageDialog - * @param dialogService Instance of the dialog service that will be used to open the dialog - * @param config Configuration for the dialog - */ -export function openAdjustStorageDialog( - dialogService: DialogService, - config: DialogConfig, -) { - return dialogService.open(AdjustStorageDialogComponent, config); + static open = ( + dialogService: DialogService, + dialogConfig: DialogConfig, + ) => + dialogService.open(AdjustStorageDialogComponent, dialogConfig); } diff --git a/apps/web/src/app/billing/shared/billing-shared.module.ts b/apps/web/src/app/billing/shared/billing-shared.module.ts index b9c235943ad..9a69755b209 100644 --- a/apps/web/src/app/billing/shared/billing-shared.module.ts +++ b/apps/web/src/app/billing/shared/billing-shared.module.ts @@ -6,13 +6,10 @@ import { HeaderModule } from "../../layouts/header/header.module"; import { SharedModule } from "../../shared"; import { AddCreditDialogComponent } from "./add-credit-dialog.component"; -import { AdjustPaymentDialogV2Component } from "./adjust-payment-dialog/adjust-payment-dialog-v2.component"; import { AdjustPaymentDialogComponent } from "./adjust-payment-dialog/adjust-payment-dialog.component"; -import { AdjustStorageDialogV2Component } from "./adjust-storage-dialog/adjust-storage-dialog-v2.component"; import { AdjustStorageDialogComponent } from "./adjust-storage-dialog/adjust-storage-dialog.component"; import { BillingHistoryComponent } from "./billing-history.component"; import { OffboardingSurveyComponent } from "./offboarding-survey.component"; -import { PaymentV2Component } from "./payment/payment-v2.component"; import { PaymentComponent } from "./payment/payment.component"; import { PaymentMethodComponent } from "./payment-method.component"; import { IndividualSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/individual-self-hosting-license-uploader.component"; @@ -26,40 +23,35 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac @NgModule({ imports: [ SharedModule, - PaymentComponent, TaxInfoComponent, HeaderModule, BannerModule, - PaymentV2Component, + PaymentComponent, VerifyBankAccountComponent, ], declarations: [ AddCreditDialogComponent, - AdjustPaymentDialogComponent, - AdjustStorageDialogComponent, BillingHistoryComponent, PaymentMethodComponent, SecretsManagerSubscribeComponent, UpdateLicenseComponent, UpdateLicenseDialogComponent, OffboardingSurveyComponent, - AdjustPaymentDialogV2Component, - AdjustStorageDialogV2Component, + AdjustPaymentDialogComponent, + AdjustStorageDialogComponent, IndividualSelfHostingLicenseUploaderComponent, OrganizationSelfHostingLicenseUploaderComponent, ], exports: [ SharedModule, - PaymentComponent, TaxInfoComponent, - AdjustStorageDialogComponent, BillingHistoryComponent, SecretsManagerSubscribeComponent, UpdateLicenseComponent, UpdateLicenseDialogComponent, OffboardingSurveyComponent, VerifyBankAccountComponent, - PaymentV2Component, + PaymentComponent, IndividualSelfHostingLicenseUploaderComponent, OrganizationSelfHostingLicenseUploaderComponent, ], diff --git a/apps/web/src/app/billing/shared/index.ts b/apps/web/src/app/billing/shared/index.ts index 69a4b93bec8..54ab5bc0a2a 100644 --- a/apps/web/src/app/billing/shared/index.ts +++ b/apps/web/src/app/billing/shared/index.ts @@ -1,5 +1,4 @@ export * from "./billing-shared.module"; export * from "./payment-method.component"; -export * from "./payment/payment.component"; export * from "./sm-subscribe.component"; export * from "./tax-info.component"; diff --git a/apps/web/src/app/billing/shared/payment-method.component.ts b/apps/web/src/app/billing/shared/payment-method.component.ts index 4a53e503e4e..c5ec942f8b7 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.ts +++ b/apps/web/src/app/billing/shared/payment-method.component.ts @@ -29,8 +29,8 @@ import { TrialFlowService } from "../services/trial-flow.service"; import { AddCreditDialogResult, openAddCreditDialog } from "./add-credit-dialog.component"; import { - AdjustPaymentDialogResult, - openAdjustPaymentDialog, + AdjustPaymentDialogComponent, + AdjustPaymentDialogResultType, } from "./adjust-payment-dialog/adjust-payment-dialog.component"; @Component({ @@ -170,14 +170,16 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { }; changePayment = async () => { - const dialogRef = openAdjustPaymentDialog(this.dialogService, { + const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, { data: { organizationId: this.organizationId, - currentType: this.paymentSource !== null ? this.paymentSource.type : null, + initialPaymentMethod: this.paymentSource !== null ? this.paymentSource.type : null, }, }); + const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustPaymentDialogResult.Adjusted) { + + if (result === AdjustPaymentDialogResultType.Submitted) { this.location.replaceState(this.location.path(), "", {}); if (this.launchPaymentModalAutomatically && !this.organization.enabled) { await this.syncService.fullSync(true); diff --git a/apps/web/src/app/billing/shared/payment/payment-label-v2.component.html b/apps/web/src/app/billing/shared/payment/payment-label.component.html similarity index 100% rename from apps/web/src/app/billing/shared/payment/payment-label-v2.component.html rename to apps/web/src/app/billing/shared/payment/payment-label.component.html diff --git a/apps/web/src/app/billing/shared/payment/payment-label-v2.component.ts b/apps/web/src/app/billing/shared/payment/payment-label.component.ts similarity index 89% rename from apps/web/src/app/billing/shared/payment/payment-label-v2.component.ts rename to apps/web/src/app/billing/shared/payment/payment-label.component.ts index f4d0f097766..179011e1144 100644 --- a/apps/web/src/app/billing/shared/payment/payment-label-v2.component.ts +++ b/apps/web/src/app/billing/shared/payment/payment-label.component.ts @@ -15,12 +15,12 @@ import { SharedModule } from "../../../shared"; * the `ExtensionRefresh` flag is set. */ @Component({ - selector: "app-payment-label-v2", - templateUrl: "./payment-label-v2.component.html", + selector: "app-payment-label", + templateUrl: "./payment-label.component.html", standalone: true, imports: [FormFieldModule, SharedModule], }) -export class PaymentLabelV2 implements OnInit { +export class PaymentLabelComponent implements OnInit { /** `id` of the associated input */ @Input({ required: true }) for: string; /** Displays required text on the label */ diff --git a/apps/web/src/app/billing/shared/payment/payment-v2.component.html b/apps/web/src/app/billing/shared/payment/payment-v2.component.html deleted file mode 100644 index 9804e6bc86f..00000000000 --- a/apps/web/src/app/billing/shared/payment/payment-v2.component.html +++ /dev/null @@ -1,152 +0,0 @@ -
-
- - - - - {{ "creditCard" | i18n }} - - - - - - {{ "bankAccount" | i18n }} - - - - - - {{ "payPal" | i18n }} - - - - - - {{ "accountCredit" | i18n }} - - - -
- - -
-
- - {{ "number" | i18n }} - -
-
-
- Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay -
-
- - {{ "expiration" | i18n }} - -
-
-
- - {{ "securityCodeSlashCVV" | i18n }} - - - - -
-
-
-
- - - - {{ "verifyBankAccountWithStatementDescriptorWarning" | i18n }} - -
- - {{ "routingNumber" | i18n }} - - - - {{ "accountNumber" | i18n }} - - - - {{ "accountHolderName" | i18n }} - - - - {{ "bankAccountType" | i18n }} - - - - - - -
-
- - -
-
- {{ "paypalClickSubmit" | i18n }} -
-
- - - - {{ "makeSureEnoughCredit" | i18n }} - - - -
diff --git a/apps/web/src/app/billing/shared/payment/payment-v2.component.ts b/apps/web/src/app/billing/shared/payment/payment-v2.component.ts deleted file mode 100644 index f65a5743c35..00000000000 --- a/apps/web/src/app/billing/shared/payment/payment-v2.component.ts +++ /dev/null @@ -1,205 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { Subject } from "rxjs"; -import { takeUntil } from "rxjs/operators"; - -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { TokenizedPaymentSourceRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-source.request"; - -import { SharedModule } from "../../../shared"; -import { BillingServicesModule, BraintreeService, StripeService } from "../../services"; - -import { PaymentLabelV2 } from "./payment-label-v2.component"; - -/** - * Render a form that allows the user to enter their payment method, tokenize it against one of our payment providers and, - * optionally, submit it using the {@link onSubmit} function if it is provided. - * - * This component is meant to replace the existing {@link PaymentComponent} which is using the deprecated Stripe Sources API. - */ -@Component({ - selector: "app-payment-v2", - templateUrl: "./payment-v2.component.html", - standalone: true, - imports: [BillingServicesModule, SharedModule, PaymentLabelV2], -}) -export class PaymentV2Component implements OnInit, OnDestroy { - /** Show account credit as a payment option. */ - @Input() showAccountCredit: boolean = true; - /** Show bank account as a payment option. */ - @Input() showBankAccount: boolean = true; - /** Show PayPal as a payment option. */ - @Input() showPayPal: boolean = true; - - /** The payment method selected by default when the component renders. */ - @Input() private initialPaymentMethod: PaymentMethodType = PaymentMethodType.Card; - /** If provided, will be invoked with the tokenized payment source during form submission. */ - @Input() protected onSubmit?: (request: TokenizedPaymentSourceRequest) => Promise; - - @Output() submitted = new EventEmitter(); - - private destroy$ = new Subject(); - - protected formGroup = new FormGroup({ - paymentMethod: new FormControl(null), - bankInformation: new FormGroup({ - routingNumber: new FormControl("", [Validators.required]), - accountNumber: new FormControl("", [Validators.required]), - accountHolderName: new FormControl("", [Validators.required]), - accountHolderType: new FormControl("", [Validators.required]), - }), - }); - - protected PaymentMethodType = PaymentMethodType; - - constructor( - private billingApiService: BillingApiServiceAbstraction, - private braintreeService: BraintreeService, - private stripeService: StripeService, - ) {} - - ngOnInit(): void { - this.formGroup.controls.paymentMethod.patchValue(this.initialPaymentMethod); - - this.stripeService.loadStripe( - { - cardNumber: "#stripe-card-number", - cardExpiry: "#stripe-card-expiry", - cardCvc: "#stripe-card-cvc", - }, - this.initialPaymentMethod === PaymentMethodType.Card, - ); - - if (this.showPayPal) { - this.braintreeService.loadBraintree( - "#braintree-container", - this.initialPaymentMethod === PaymentMethodType.PayPal, - ); - } - - this.formGroup - .get("paymentMethod") - .valueChanges.pipe(takeUntil(this.destroy$)) - .subscribe((type) => { - this.onPaymentMethodChange(type); - }); - } - - /** Programmatically select the provided payment method. */ - select = (paymentMethod: PaymentMethodType) => { - this.formGroup.get("paymentMethod").patchValue(paymentMethod); - }; - - protected submit = async () => { - const { type, token } = await this.tokenize(); - await this.onSubmit?.({ type, token }); - this.submitted.emit(type); - }; - - /** - * Tokenize the payment method information entered by the user against one of our payment providers. - * - * - {@link PaymentMethodType.Card} => [Stripe.confirmCardSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_card_setup} - * - {@link PaymentMethodType.BankAccount} => [Stripe.confirmUsBankAccountSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_us_bank_account_setup} - * - {@link PaymentMethodType.PayPal} => [Braintree.requestPaymentMethod]{@link https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html#requestPaymentMethod} - * */ - async tokenize(): Promise<{ type: PaymentMethodType; token: string }> { - const type = this.selected; - - if (this.usingStripe) { - const clientSecret = await this.billingApiService.createSetupIntent(type); - - if (this.usingBankAccount) { - this.formGroup.markAllAsTouched(); - if (this.formGroup.valid) { - const token = await this.stripeService.setupBankAccountPaymentMethod(clientSecret, { - accountHolderName: this.formGroup.value.bankInformation.accountHolderName, - routingNumber: this.formGroup.value.bankInformation.routingNumber, - accountNumber: this.formGroup.value.bankInformation.accountNumber, - accountHolderType: this.formGroup.value.bankInformation.accountHolderType, - }); - return { - type, - token, - }; - } else { - throw "Invalid input provided, Please ensure all required fields are filled out correctly and try again."; - } - } - - if (this.usingCard) { - const token = await this.stripeService.setupCardPaymentMethod(clientSecret); - return { - type, - token, - }; - } - } - - if (this.usingPayPal) { - const token = await this.braintreeService.requestPaymentMethod(); - return { - type, - token, - }; - } - - if (this.usingAccountCredit) { - return { - type: PaymentMethodType.Credit, - token: null, - }; - } - - return null; - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - this.stripeService.unloadStripe(); - if (this.showPayPal) { - this.braintreeService.unloadBraintree(); - } - } - - private onPaymentMethodChange(type: PaymentMethodType): void { - switch (type) { - case PaymentMethodType.Card: { - this.stripeService.mountElements(); - break; - } - case PaymentMethodType.PayPal: { - this.braintreeService.createDropin(); - break; - } - } - } - - get selected(): PaymentMethodType { - return this.formGroup.value.paymentMethod; - } - - protected get usingAccountCredit(): boolean { - return this.selected === PaymentMethodType.Credit; - } - - protected get usingBankAccount(): boolean { - return this.selected === PaymentMethodType.BankAccount; - } - - protected get usingCard(): boolean { - return this.selected === PaymentMethodType.Card; - } - - protected get usingPayPal(): boolean { - return this.selected === PaymentMethodType.PayPal; - } - - private get usingStripe(): boolean { - return this.usingBankAccount || this.usingCard; - } -} diff --git a/apps/web/src/app/billing/shared/payment/payment.component.html b/apps/web/src/app/billing/shared/payment/payment.component.html index d4853713579..af261155171 100644 --- a/apps/web/src/app/billing/shared/payment/payment.component.html +++ b/apps/web/src/app/billing/shared/payment/payment.component.html @@ -1,96 +1,125 @@ -
-
- - +
+
+ + - {{ "creditCard" | i18n }} + {{ "creditCard" | i18n }} + - + - {{ "bankAccount" | i18n }} + {{ "bankAccount" | i18n }} + - - PayPal + + + + {{ "payPal" | i18n }} + - + - {{ "accountCredit" | i18n }} + {{ "accountCredit" | i18n }} +
- -
-
- {{ - "number" | i18n - }} -
+ + +
+
+ + {{ "number" | i18n }} + +
-
+
Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay
-
- {{ - "expiration" | i18n - }} -
+
+ + {{ "expiration" | i18n }} + +
-
- +
+ {{ "securityCodeSlashCVV" | i18n }} - -
+
+
- + + - {{ "verifyBankAccountInitialDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }} + {{ "verifyBankAccountWithStatementDescriptorWarning" | i18n }} -
- +
+ {{ "routingNumber" | i18n }} - + - + {{ "accountNumber" | i18n }} - + - + {{ "accountHolderName" | i18n }} - - + {{ "bankAccountType" | i18n }} - +
- + +
-
+
{{ "paypalClickSubmit" | i18n }}
- - + + + {{ "makeSureEnoughCredit" | i18n }} - + -
+ + diff --git a/apps/web/src/app/billing/shared/payment/payment.component.ts b/apps/web/src/app/billing/shared/payment/payment.component.ts index e067a5ee490..c11dfddb6cc 100644 --- a/apps/web/src/app/billing/shared/payment/payment.component.ts +++ b/apps/web/src/app/billing/shared/payment/payment.component.ts @@ -1,330 +1,203 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, Input, OnDestroy, OnInit } from "@angular/core"; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { Subject, takeUntil } from "rxjs"; +import { Subject } from "rxjs"; +import { takeUntil } from "rxjs/operators"; -import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { TokenizedPaymentSourceRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-source.request"; import { SharedModule } from "../../../shared"; +import { BillingServicesModule, BraintreeService, StripeService } from "../../services"; -import { PaymentLabelV2 } from "./payment-label-v2.component"; +import { PaymentLabelComponent } from "./payment-label.component"; +/** + * Render a form that allows the user to enter their payment method, tokenize it against one of our payment providers and, + * optionally, submit it using the {@link onSubmit} function if it is provided. + */ @Component({ selector: "app-payment", - templateUrl: "payment.component.html", + templateUrl: "./payment.component.html", standalone: true, - imports: [SharedModule, PaymentLabelV2], + imports: [BillingServicesModule, SharedModule, PaymentLabelComponent], }) export class PaymentComponent implements OnInit, OnDestroy { - @Input() showMethods = true; - @Input() showOptions = true; - @Input() hideBank = false; - @Input() hidePaypal = false; - @Input() hideCredit = false; - @Input() trialFlow = false; + /** Show account credit as a payment option. */ + @Input() showAccountCredit: boolean = true; + /** Show bank account as a payment option. */ + @Input() showBankAccount: boolean = true; + /** Show PayPal as a payment option. */ + @Input() showPayPal: boolean = true; - @Input() - set method(value: PaymentMethodType) { - this._method = value; - this.paymentForm?.controls.method.setValue(value, { emitEvent: false }); - } + /** The payment method selected by default when the component renders. */ + @Input() private initialPaymentMethod: PaymentMethodType = PaymentMethodType.Card; + /** If provided, will be invoked with the tokenized payment source during form submission. */ + @Input() protected onSubmit?: (request: TokenizedPaymentSourceRequest) => Promise; - get method(): PaymentMethodType { - return this._method; - } - private _method: PaymentMethodType = PaymentMethodType.Card; + @Output() submitted = new EventEmitter(); private destroy$ = new Subject(); - protected paymentForm = new FormGroup({ - method: new FormControl(this.method), - bank: new FormGroup({ - routing_number: new FormControl(null, [Validators.required]), - account_number: new FormControl(null, [Validators.required]), - account_holder_name: new FormControl(null, [Validators.required]), - account_holder_type: new FormControl("", [Validators.required]), - currency: new FormControl("USD"), - country: new FormControl("US"), + + protected formGroup = new FormGroup({ + paymentMethod: new FormControl(null), + bankInformation: new FormGroup({ + routingNumber: new FormControl("", [Validators.required]), + accountNumber: new FormControl("", [Validators.required]), + accountHolderName: new FormControl("", [Validators.required]), + accountHolderType: new FormControl("", [Validators.required]), }), }); - paymentMethodType = PaymentMethodType; - private btScript: HTMLScriptElement; - private btInstance: any = null; - private stripeScript: HTMLScriptElement; - private stripe: any = null; - private stripeElements: any = null; - private stripeCardNumberElement: any = null; - private stripeCardExpiryElement: any = null; - private stripeCardCvcElement: any = null; - private StripeElementStyle: any; - private StripeElementClasses: any; + protected PaymentMethodType = PaymentMethodType; constructor( - private apiService: ApiService, - private logService: LogService, - private themingService: AbstractThemingService, - private configService: ConfigService, - ) { - this.stripeScript = window.document.createElement("script"); - this.stripeScript.src = "https://js.stripe.com/v3/?advancedFraudSignals=false"; - this.stripeScript.async = true; - this.stripeScript.onload = async () => { - this.stripe = (window as any).Stripe(process.env.STRIPE_KEY); - this.stripeElements = this.stripe.elements(); - await this.setStripeElement(); - }; - this.btScript = window.document.createElement("script"); - this.btScript.src = `scripts/dropin.js?cache=${process.env.CACHE_TAG}`; - this.btScript.async = true; - this.StripeElementStyle = { - base: { - color: null, - fontFamily: - '"DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif, ' + - '"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', - fontSize: "16px", - fontSmoothing: "antialiased", - "::placeholder": { - color: null, - }, + private billingApiService: BillingApiServiceAbstraction, + private braintreeService: BraintreeService, + private stripeService: StripeService, + ) {} + + ngOnInit(): void { + this.formGroup.controls.paymentMethod.patchValue(this.initialPaymentMethod); + + this.stripeService.loadStripe( + { + cardNumber: "#stripe-card-number", + cardExpiry: "#stripe-card-expiry", + cardCvc: "#stripe-card-cvc", }, - invalid: { - color: null, - }, - }; - this.StripeElementClasses = { - focus: "is-focused", - empty: "is-empty", - invalid: "is-invalid", - }; - } - async ngOnInit() { - if (!this.showOptions) { - this.hidePaypal = this.method !== PaymentMethodType.PayPal; - this.hideBank = this.method !== PaymentMethodType.BankAccount; - this.hideCredit = this.method !== PaymentMethodType.Credit; - } - this.subscribeToTheme(); - window.document.head.appendChild(this.stripeScript); - if (!this.hidePaypal) { - window.document.head.appendChild(this.btScript); + this.initialPaymentMethod === PaymentMethodType.Card, + ); + + if (this.showPayPal) { + this.braintreeService.loadBraintree( + "#braintree-container", + this.initialPaymentMethod === PaymentMethodType.PayPal, + ); } - this.paymentForm - .get("method") + + this.formGroup + .get("paymentMethod") .valueChanges.pipe(takeUntil(this.destroy$)) - .subscribe((v) => { - this.method = v; - this.changeMethod(); + .subscribe((type) => { + this.onPaymentMethodChange(type); }); } - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - window.document.head.removeChild(this.stripeScript); - window.setTimeout(() => { - Array.from(window.document.querySelectorAll("iframe")).forEach((el) => { - if (el.src != null && el.src.indexOf("stripe") > -1) { - try { - window.document.body.removeChild(el); - } catch (e) { - this.logService.error(e); - } - } - }); - }, 500); - if (!this.hidePaypal) { - window.document.head.removeChild(this.btScript); - window.setTimeout(() => { - Array.from(window.document.head.querySelectorAll("script")).forEach((el) => { - if (el.src != null && el.src.indexOf("paypal") > -1) { - try { - window.document.head.removeChild(el); - } catch (e) { - this.logService.error(e); - } - } - }); - const btStylesheet = window.document.head.querySelector("#braintree-dropin-stylesheet"); - if (btStylesheet != null) { - try { - window.document.head.removeChild(btStylesheet); - } catch (e) { - this.logService.error(e); - } + /** Programmatically select the provided payment method. */ + select = (paymentMethod: PaymentMethodType) => { + this.formGroup.get("paymentMethod").patchValue(paymentMethod); + }; + + protected submit = async () => { + const { type, token } = await this.tokenize(); + await this.onSubmit?.({ type, token }); + this.submitted.emit(type); + }; + + /** + * Tokenize the payment method information entered by the user against one of our payment providers. + * + * - {@link PaymentMethodType.Card} => [Stripe.confirmCardSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_card_setup} + * - {@link PaymentMethodType.BankAccount} => [Stripe.confirmUsBankAccountSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_us_bank_account_setup} + * - {@link PaymentMethodType.PayPal} => [Braintree.requestPaymentMethod]{@link https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html#requestPaymentMethod} + * */ + async tokenize(): Promise<{ type: PaymentMethodType; token: string }> { + const type = this.selected; + + if (this.usingStripe) { + const clientSecret = await this.billingApiService.createSetupIntent(type); + + if (this.usingBankAccount) { + this.formGroup.markAllAsTouched(); + if (this.formGroup.valid) { + const token = await this.stripeService.setupBankAccountPaymentMethod(clientSecret, { + accountHolderName: this.formGroup.value.bankInformation.accountHolderName, + routingNumber: this.formGroup.value.bankInformation.routingNumber, + accountNumber: this.formGroup.value.bankInformation.accountNumber, + accountHolderType: this.formGroup.value.bankInformation.accountHolderType, + }); + return { + type, + token, + }; + } else { + throw "Invalid input provided, Please ensure all required fields are filled out correctly and try again."; } - }, 500); + } + + if (this.usingCard) { + const token = await this.stripeService.setupCardPaymentMethod(clientSecret); + return { + type, + token, + }; + } } + + if (this.usingPayPal) { + const token = await this.braintreeService.requestPaymentMethod(); + return { + type, + token, + }; + } + + if (this.usingAccountCredit) { + return { + type: PaymentMethodType.Credit, + token: null, + }; + } + + return null; } - changeMethod() { - this.btInstance = null; - if (this.method === PaymentMethodType.PayPal) { - window.setTimeout(() => { - (window as any).braintree.dropin.create( - { - authorization: process.env.BRAINTREE_KEY, - container: "#bt-dropin-container", - paymentOptionPriority: ["paypal"], - paypal: { - flow: "vault", - buttonStyle: { - label: "pay", - size: "medium", - shape: "pill", - color: "blue", - tagline: "false", - }, - }, - }, - (createErr: any, instance: any) => { - if (createErr != null) { - // eslint-disable-next-line - console.error(createErr); - return; - } - this.btInstance = instance; - }, - ); - }, 250); - } else { - void this.setStripeElement(); + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + this.stripeService.unloadStripe(); + if (this.showPayPal) { + this.braintreeService.unloadBraintree(); } } - createPaymentToken(): Promise<[string, PaymentMethodType]> { - return new Promise((resolve, reject) => { - if (this.method === PaymentMethodType.Credit) { - resolve([null, this.method]); - } else if (this.method === PaymentMethodType.PayPal) { - this.btInstance - .requestPaymentMethod() - .then((payload: any) => { - resolve([payload.nonce, this.method]); - }) - .catch((err: any) => { - reject(err.message); - }); - } else if ( - this.method === PaymentMethodType.Card || - this.method === PaymentMethodType.BankAccount - ) { - if (this.method === PaymentMethodType.Card) { - // 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.apiService - .postSetupPayment() - .then((clientSecret) => - this.stripe.handleCardSetup(clientSecret, this.stripeCardNumberElement), - ) - .then((result: any) => { - if (result.error) { - reject(result.error.message); - } else if (result.setupIntent && result.setupIntent.status === "succeeded") { - resolve([result.setupIntent.payment_method, this.method]); - } else { - reject(); - } - }); - } else { - this.stripe - .createToken("bank_account", this.paymentForm.get("bank").value) - .then((result: any) => { - if (result.error) { - reject(result.error.message); - } else if (result.token && result.token.id != null) { - resolve([result.token.id, this.method]); - } else { - reject(); - } - }); - } + private onPaymentMethodChange(type: PaymentMethodType): void { + switch (type) { + case PaymentMethodType.Card: { + this.stripeService.mountElements(); + break; + } + case PaymentMethodType.PayPal: { + this.braintreeService.createDropin(); + break; } - }); + } } - handleStripeCardPayment(clientSecret: string, successCallback: () => Promise): Promise { - return new Promise((resolve, reject) => { - if (this.showMethods && this.stripeCardNumberElement == null) { - reject(); - return; - } - const handleCardPayment = () => - this.showMethods - ? this.stripe.handleCardSetup(clientSecret, this.stripeCardNumberElement) - : this.stripe.handleCardSetup(clientSecret); - return handleCardPayment().then(async (result: any) => { - if (result.error) { - reject(result.error.message); - } else if (result.paymentIntent && result.paymentIntent.status === "succeeded") { - if (successCallback != null) { - await successCallback(); - } - resolve(); - } else { - reject(); - } - }); - }); + get selected(): PaymentMethodType { + return this.formGroup.value.paymentMethod; } - private async setStripeElement() { - const extensionRefreshFlag = await this.configService.getFeatureFlag( - FeatureFlag.ExtensionRefresh, - ); + protected get usingAccountCredit(): boolean { + return this.selected === PaymentMethodType.Credit; + } - // Apply unique styles for extension refresh - if (extensionRefreshFlag) { - this.StripeElementStyle.base.fontWeight = "500"; - this.StripeElementClasses.base = "v2"; - } + protected get usingBankAccount(): boolean { + return this.selected === PaymentMethodType.BankAccount; + } - window.setTimeout(() => { - if (this.showMethods && this.method === PaymentMethodType.Card) { - if (this.stripeCardNumberElement == null) { - this.stripeCardNumberElement = this.stripeElements.create("cardNumber", { - style: this.StripeElementStyle, - classes: this.StripeElementClasses, - placeholder: "", - }); - } - if (this.stripeCardExpiryElement == null) { - this.stripeCardExpiryElement = this.stripeElements.create("cardExpiry", { - style: this.StripeElementStyle, - classes: this.StripeElementClasses, - }); - } - if (this.stripeCardCvcElement == null) { - this.stripeCardCvcElement = this.stripeElements.create("cardCvc", { - style: this.StripeElementStyle, - classes: this.StripeElementClasses, - placeholder: "", - }); - } - this.stripeCardNumberElement.mount("#stripe-card-number-element"); - this.stripeCardExpiryElement.mount("#stripe-card-expiry-element"); - this.stripeCardCvcElement.mount("#stripe-card-cvc-element"); - } - }, 50); + protected get usingCard(): boolean { + return this.selected === PaymentMethodType.Card; + } + + protected get usingPayPal(): boolean { + return this.selected === PaymentMethodType.PayPal; } - private subscribeToTheme() { - this.themingService.theme$.pipe(takeUntil(this.destroy$)).subscribe(() => { - const style = getComputedStyle(document.documentElement); - this.StripeElementStyle.base.color = `rgb(${style.getPropertyValue("--color-text-main")})`; - this.StripeElementStyle.base["::placeholder"].color = `rgb(${style.getPropertyValue( - "--color-text-muted", - )})`; - this.StripeElementStyle.invalid.color = `rgb(${style.getPropertyValue("--color-text-main")})`; - this.StripeElementStyle.invalid.borderColor = `rgb(${style.getPropertyValue( - "--color-danger-600", - )})`; - }); + private get usingStripe(): boolean { + return this.usingBankAccount || this.usingCard; } } diff --git a/apps/web/src/app/components/environment-selector/environment-selector.component.html b/apps/web/src/app/components/environment-selector/environment-selector.component.html index a1fb1a8a0f3..91675bd4945 100644 --- a/apps/web/src/app/components/environment-selector/environment-selector.component.html +++ b/apps/web/src/app/components/environment-selector/environment-selector.component.html @@ -6,10 +6,9 @@ [attr.href]=" region == currentRegion ? 'javascript:void(0)' : region.urls.webVault + routeAndParams " - class="pr-4" > diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 8f21dfa2c8b..1581d4ad8cd 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -66,6 +66,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { AppIdService as DefaultAppIdService } from "@bitwarden/common/platform/services/app-id.service"; @@ -73,7 +74,9 @@ import { MemoryStorageService } from "@bitwarden/common/platform/services/memory // eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; +import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory"; import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory"; +import { NoopSdkLoadService } from "@bitwarden/common/platform/services/sdk/noop-sdk-load.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; /* eslint-disable import/no-restricted-paths -- Implementation for memory storage */ import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state"; @@ -110,7 +113,7 @@ import { WebProcessReloadService } from "../key-management/services/web-process- import { WebBiometricsService } from "../key-management/web-biometric.service"; import { WebEnvironmentService } from "../platform/web-environment.service"; import { WebMigrationRunner } from "../platform/web-migration-runner"; -import { WebSdkClientFactory } from "../platform/web-sdk-client-factory"; +import { WebSdkLoadService } from "../platform/web-sdk-load.service"; import { WebStorageServiceProvider } from "../platform/web-storage-service.provider"; import { EventService } from "./event.service"; @@ -288,9 +291,14 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultCollectionAdminService, deps: [ApiService, KeyServiceAbstraction, EncryptService, CollectionService], }), + safeProvider({ + provide: SdkLoadService, + useClass: flagEnabled("sdk") ? WebSdkLoadService : NoopSdkLoadService, + deps: [], + }), safeProvider({ provide: SdkClientFactory, - useClass: flagEnabled("sdk") ? WebSdkClientFactory : NoopSdkClientFactory, + useClass: flagEnabled("sdk") ? DefaultSdkClientFactory : NoopSdkClientFactory, deps: [], }), safeProvider({ diff --git a/apps/web/src/app/core/init.service.ts b/apps/web/src/app/core/init.service.ts index b3e6d691f75..0d6063b4ac3 100644 --- a/apps/web/src/app/core/init.service.ts +++ b/apps/web/src/app/core/init.service.ts @@ -10,6 +10,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; @@ -35,11 +36,13 @@ export class InitService { private userAutoUnlockKeyService: UserAutoUnlockKeyService, private accountService: AccountService, private versionService: VersionService, + private sdkLoadService: SdkLoadService, @Inject(DOCUMENT) private document: Document, ) {} init() { return async () => { + await this.sdkLoadService.load(); await this.stateService.init(); const activeAccount = await firstValueFrom(this.accountService.activeAccount$); diff --git a/apps/web/src/app/key-management/key-rotation/request/update-key.request.ts b/apps/web/src/app/key-management/key-rotation/request/update-key.request.ts index d407a709d4c..9358c4b200e 100644 --- a/apps/web/src/app/key-management/key-rotation/request/update-key.request.ts +++ b/apps/web/src/app/key-management/key-rotation/request/update-key.request.ts @@ -2,8 +2,14 @@ // @ts-strict-ignore import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common"; import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { SendWithIdRequest } from "@bitwarden/common/src/tools/send/models/request/send-with-id.request"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { CipherWithIdRequest } from "@bitwarden/common/src/vault/models/request/cipher-with-id.request"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { FolderWithIdRequest } from "@bitwarden/common/src/vault/models/request/folder-with-id.request"; import { EmergencyAccessWithIdRequest } from "../../../auth/emergency-access/request/emergency-access-update.request"; diff --git a/apps/web/src/app/layouts/frontend-layout.component.html b/apps/web/src/app/layouts/frontend-layout.component.html index 72f0f1f1da3..d19af54f5df 100644 --- a/apps/web/src/app/layouts/frontend-layout.component.html +++ b/apps/web/src/app/layouts/frontend-layout.component.html @@ -1,6 +1,8 @@ -
+ +
- © {{ year }} Bitwarden Inc.
- {{ "versionNumber" | i18n: version }} -
+ +
© {{ year }} Bitwarden Inc.
+
{{ version }}
+ diff --git a/apps/web/src/app/layouts/org-switcher/org-switcher.component.html b/apps/web/src/app/layouts/org-switcher/org-switcher.component.html index f34d32f5983..ef702c7e593 100644 --- a/apps/web/src/app/layouts/org-switcher/org-switcher.component.html +++ b/apps/web/src/app/layouts/org-switcher/org-switcher.component.html @@ -10,7 +10,7 @@ @@ -27,7 +27,7 @@ diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts index 382ce8e026b..6e21c6c142a 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts @@ -7,6 +7,8 @@ import { BehaviorSubject } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { IconButtonModule, NavigationModule } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { NavItemComponent } from "@bitwarden/components/src/navigation/nav-item.component"; import { ProductSwitcherItem, ProductSwitcherService } from "../shared/product-switcher.service"; diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts index eb486efe454..181779c7c2e 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts @@ -13,6 +13,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { SyncService } from "@bitwarden/common/platform/sync"; import { UserId } from "@bitwarden/common/types/guid"; import { LayoutComponent, NavigationModule } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { I18nMockService } from "@bitwarden/components/src/utils/i18n-mock.service"; import { ProductSwitcherService } from "../shared/product-switcher.service"; diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts b/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts index 80155166394..834571e2cb4 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts @@ -1,5 +1,7 @@ import { AfterViewInit, ChangeDetectorRef, Component, Input } from "@angular/core"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { IconButtonType } from "@bitwarden/components/src/icon-button/icon-button.component"; import { ProductSwitcherService } from "./shared/product-switcher.service"; diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts index e8cefa44146..44467bb2b29 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts @@ -13,6 +13,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { SyncService } from "@bitwarden/common/platform/sync"; import { UserId } from "@bitwarden/common/types/guid"; import { IconButtonModule, LinkModule, MenuModule } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { I18nMockService } from "@bitwarden/components/src/utils/i18n-mock.service"; import { ProductSwitcherContentComponent } from "./product-switcher-content.component"; diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 8a678a3b045..d03548faf9a 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -1,7 +1,7 @@ import { NgModule } from "@angular/core"; import { Route, RouterModule, Routes } from "@angular/router"; -import { TwoFactorTimeoutComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-expired.component"; +import { AuthenticationTimeoutComponent } from "@bitwarden/angular/auth/components/authentication-timeout.component"; import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap"; import { authGuard, @@ -9,7 +9,9 @@ import { redirectGuard, tdeDecryptionRequiredGuard, unauthGuardFn, + activeAuthGuard, } from "@bitwarden/angular/auth/guards"; +import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { generatorSwap } from "@bitwarden/angular/tools/generator/generator-swap"; import { twofactorRefactorSwap } from "@bitwarden/angular/utils/two-factor-component-refactor-route-swap"; import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; @@ -37,7 +39,10 @@ import { SsoComponent, VaultIcon, LoginDecryptionOptionsComponent, + NewDeviceVerificationComponent, + DeviceVerificationIcon, } from "@bitwarden/auth/angular"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { LockComponent } from "@bitwarden/key-management/angular"; import { NewDeviceVerificationNoticePageOneComponent, @@ -538,12 +543,12 @@ const routes: Routes = [ } satisfies RouteDataProperties & AnonLayoutWrapperData, }, { - path: "2fa-timeout", + path: "authentication-timeout", canActivate: [unauthGuardFn()], children: [ { path: "", - component: TwoFactorTimeoutComponent, + component: AuthenticationTimeoutComponent, }, { path: "", @@ -580,6 +585,29 @@ const routes: Routes = [ titleId: "recoverAccountTwoStep", } satisfies RouteDataProperties & AnonLayoutWrapperData, }, + { + path: "device-verification", + canActivate: [ + canAccessFeature(FeatureFlag.NewDeviceVerification), + unauthGuardFn(), + activeAuthGuard(), + ], + children: [ + { + path: "", + component: NewDeviceVerificationComponent, + }, + ], + data: { + pageIcon: DeviceVerificationIcon, + pageTitle: { + key: "verifyIdentity", + }, + pageSubtitle: { + key: "weDontRecognizeThisDevice", + }, + } satisfies RouteDataProperties & AnonLayoutWrapperData, + }, { path: "accept-emergency", canActivate: [deepLinkGuard()], diff --git a/apps/web/src/app/platform/web-sdk-client-factory.ts b/apps/web/src/app/platform/web-sdk-client-factory.ts deleted file mode 100644 index 0dd43ecbb92..00000000000 --- a/apps/web/src/app/platform/web-sdk-client-factory.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; -import * as sdk from "@bitwarden/sdk-internal"; - -/** - * SDK client factory with a js fallback for when WASM is not supported. - */ -export class WebSdkClientFactory implements SdkClientFactory { - async createSdkClient( - ...args: ConstructorParameters - ): Promise { - const module = await load(); - - (sdk as any).init(module); - - return Promise.resolve(new sdk.BitwardenClient(...args)); - } -} - -// https://stackoverflow.com/a/47880734 -const supported = (() => { - try { - if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") { - const module = new WebAssembly.Module( - Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00), - ); - if (module instanceof WebAssembly.Module) { - return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; - } - } - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e) { - // ignore - } - return false; -})(); - -async function load() { - if (supported) { - return await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"); - } else { - return await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm.js"); - } -} diff --git a/apps/web/src/app/platform/web-sdk-load.service.ts b/apps/web/src/app/platform/web-sdk-load.service.ts new file mode 100644 index 00000000000..cae3399b81e --- /dev/null +++ b/apps/web/src/app/platform/web-sdk-load.service.ts @@ -0,0 +1,31 @@ +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; +import * as sdk from "@bitwarden/sdk-internal"; + +// https://stackoverflow.com/a/47880734 +const supported = (() => { + try { + if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") { + const module = new WebAssembly.Module( + Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00), + ); + if (module instanceof WebAssembly.Module) { + return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; + } + } + } catch { + // ignore + } + return false; +})(); + +export class WebSdkLoadService implements SdkLoadService { + async load(): Promise { + let module: any; + if (supported) { + module = await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"); + } else { + module = await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm.js"); + } + (sdk as any).init(module); + } +} diff --git a/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts b/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts index 805745c369d..5edd8bc046e 100644 --- a/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts +++ b/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Guid } from "@bitwarden/common/src/types/guid"; +import { Guid } from "@bitwarden/common/types/guid"; export class RequestSMAccessRequest { OrganizationId: Guid; diff --git a/apps/web/src/app/settings/domain-rules.component.html b/apps/web/src/app/settings/domain-rules.component.html index a3bea63fb86..7e9e7d6ed64 100644 --- a/apps/web/src/app/settings/domain-rules.component.html +++ b/apps/web/src/app/settings/domain-rules.component.html @@ -43,7 +43,7 @@

{{ "customEqDomains" | i18n }}

-

{{ "globalEqDomains" | i18n }}

+

{{ "globalEqDomains" | i18n }}

; diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.spec.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.spec.ts index 838dc2c8241..5ed88c4cac9 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.spec.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.spec.ts @@ -3,12 +3,14 @@ import { mock } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; -import { CriticalAppsApiService } from "./critical-apps-api.service"; import { + PasswordHealthReportApplicationDropRequest, PasswordHealthReportApplicationId, PasswordHealthReportApplicationsRequest, PasswordHealthReportApplicationsResponse, -} from "./critical-apps.service"; +} from "../models/password-health"; + +import { CriticalAppsApiService } from "./critical-apps-api.service"; describe("CriticalAppsApiService", () => { let service: CriticalAppsApiService; @@ -76,4 +78,24 @@ describe("CriticalAppsApiService", () => { done(); }); }); + + it("should call apiService.send with correct parameters for DropCriticalApp", (done) => { + const request: PasswordHealthReportApplicationDropRequest = { + organizationId: "org1" as OrganizationId, + passwordHealthReportApplicationIds: ["123"], + }; + + apiService.send.mockReturnValue(Promise.resolve()); + + service.dropCriticalApp(request).subscribe(() => { + expect(apiService.send).toHaveBeenCalledWith( + "DELETE", + "/reports/password-health-report-application/", + request, + true, + true, + ); + done(); + }); + }); }); diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.ts index edd2cf34b56..c02a3686dfd 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.ts @@ -4,9 +4,10 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { + PasswordHealthReportApplicationDropRequest, PasswordHealthReportApplicationsRequest, PasswordHealthReportApplicationsResponse, -} from "./critical-apps.service"; +} from "../models/password-health"; export class CriticalAppsApiService { constructor(private apiService: ApiService) {} @@ -36,4 +37,16 @@ export class CriticalAppsApiService { return from(dbResponse as Promise); } + + dropCriticalApp(request: PasswordHealthReportApplicationDropRequest): Observable { + const dbResponse = this.apiService.send( + "DELETE", + "/reports/password-health-report-application/", + request, + true, + true, + ); + + return from(dbResponse as Promise); + } } diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.spec.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.spec.ts index c6c4562310e..5b89a2abb1e 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.spec.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.spec.ts @@ -12,13 +12,14 @@ import { OrganizationId } from "@bitwarden/common/types/guid"; import { OrgKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; -import { CriticalAppsApiService } from "./critical-apps-api.service"; import { - CriticalAppsService, PasswordHealthReportApplicationId, PasswordHealthReportApplicationsRequest, PasswordHealthReportApplicationsResponse, -} from "./critical-apps.service"; +} from "../models/password-health"; + +import { CriticalAppsApiService } from "./critical-apps-api.service"; +import { CriticalAppsService } from "./critical-apps.service"; describe("CriticalAppsService", () => { let service: CriticalAppsService; @@ -139,4 +140,54 @@ describe("CriticalAppsService", () => { expect(res).toHaveLength(2); }); }); + + it("should drop a critical app", async () => { + // arrange + const orgId = "org1" as OrganizationId; + const selectedUrl = "https://example.com"; + + const initialList = [ + { id: "id1", organizationId: "org1", uri: "https://example.com" }, + { id: "id2", organizationId: "org1", uri: "https://example.org" }, + ] as PasswordHealthReportApplicationsResponse[]; + + service.setAppsInListForOrg(initialList); + + // act + await service.dropCriticalApp(orgId, selectedUrl); + + // expectations + expect(criticalAppsApiService.dropCriticalApp).toHaveBeenCalledWith({ + organizationId: orgId, + passwordHealthReportApplicationIds: ["id1"], + }); + expect(service.getAppsListForOrg(orgId)).toBeTruthy(); + service.getAppsListForOrg(orgId).subscribe((res) => { + expect(res).toHaveLength(1); + expect(res[0].uri).toBe("https://example.org"); + }); + }); + + it("should not drop a critical app if it does not exist", async () => { + // arrange + const orgId = "org1" as OrganizationId; + const selectedUrl = "https://nonexistent.com"; + + const initialList = [ + { id: "id1", organizationId: "org1", uri: "https://example.com" }, + { id: "id2", organizationId: "org1", uri: "https://example.org" }, + ] as PasswordHealthReportApplicationsResponse[]; + + service.setAppsInListForOrg(initialList); + + // act + await service.dropCriticalApp(orgId, selectedUrl); + + // expectations + expect(criticalAppsApiService.dropCriticalApp).not.toHaveBeenCalled(); + expect(service.getAppsListForOrg(orgId)).toBeTruthy(); + service.getAppsListForOrg(orgId).subscribe((res) => { + expect(res).toHaveLength(2); + }); + }); }); diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.ts index 10b7d3f1fbb..00b4dc78dae 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.ts @@ -12,7 +12,6 @@ import { takeUntil, zip, } from "rxjs"; -import { Opaque } from "type-fest"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -20,6 +19,11 @@ import { OrganizationId } from "@bitwarden/common/types/guid"; import { OrgKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; +import { + PasswordHealthReportApplicationsRequest, + PasswordHealthReportApplicationsResponse, +} from "../models/password-health"; + import { CriticalAppsApiService } from "./critical-apps-api.service"; /* Retrieves and decrypts critical apps for a given organization @@ -94,6 +98,25 @@ export class CriticalAppsService { this.orgId.next(orgId); } + // Drop a critical app for a given organization + // Only one app may be dropped at a time + async dropCriticalApp(orgId: OrganizationId, selectedUrl: string) { + const app = this.criticalAppsList.value.find( + (f) => f.organizationId === orgId && f.uri === selectedUrl, + ); + + if (!app) { + return; + } + + await this.criticalAppsApiService.dropCriticalApp({ + organizationId: app.organizationId, + passwordHealthReportApplicationIds: [app.id], + }); + + this.criticalAppsList.next(this.criticalAppsList.value.filter((f) => f.uri !== selectedUrl)); + } + private retrieveCriticalApps( orgId: OrganizationId | null, ): Observable { @@ -144,16 +167,3 @@ export class CriticalAppsService { return await Promise.all(criticalAppsPromises); } } - -export interface PasswordHealthReportApplicationsRequest { - organizationId: OrganizationId; - url: string; -} - -export interface PasswordHealthReportApplicationsResponse { - id: PasswordHealthReportApplicationId; - organizationId: OrganizationId; - uri: string; -} - -export type PasswordHealthReportApplicationId = Opaque; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html index 1ab8b7e0196..d9919ef6bac 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html @@ -2,7 +2,7 @@

+ + + + + diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts index 450f0d5d660..84a32ffcd6d 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts @@ -15,12 +15,15 @@ import { ApplicationHealthReportDetailWithCriticalFlag, ApplicationHealthReportSummary, } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { DialogService, Icons, NoItemsModule, SearchModule, TableDataSource, + ToastService, } from "@bitwarden/components"; import { CardComponent } from "@bitwarden/tools-card"; import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; @@ -37,6 +40,7 @@ import { RiskInsightsTabType } from "./risk-insights.component"; selector: "tools-critical-applications", templateUrl: "./critical-applications.component.html", imports: [CardComponent, HeaderModule, SearchModule, NoItemsModule, PipesModule, SharedModule], + providers: [], }) export class CriticalApplicationsComponent implements OnInit { protected dataSource = new TableDataSource(); @@ -80,13 +84,38 @@ export class CriticalApplicationsComponent implements OnInit { }); }; + unmarkAsCriticalApp = async (hostname: string) => { + try { + await this.criticalAppsService.dropCriticalApp( + this.organizationId as OrganizationId, + hostname, + ); + } catch { + this.toastService.showToast({ + message: this.i18nService.t("unexpectedError"), + variant: "error", + title: this.i18nService.t("error"), + }); + return; + } + + this.toastService.showToast({ + message: this.i18nService.t("criticalApplicationSuccessfullyUnmarked"), + variant: "success", + title: this.i18nService.t("success"), + }); + this.dataSource.data = this.dataSource.data.filter((app) => app.applicationName !== hostname); + }; + constructor( protected activatedRoute: ActivatedRoute, protected router: Router, + protected toastService: ToastService, protected dataService: RiskInsightsDataService, protected criticalAppsService: CriticalAppsService, protected reportService: RiskInsightsReportService, protected dialogService: DialogService, + protected i18nService: I18nService, ) { this.searchControl.valueChanges .pipe(debounceTime(200), takeUntilDestroyed()) diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts index 5adb0d32945..6d39a710e24 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts @@ -2,16 +2,18 @@ import { CommonModule } from "@angular/common"; import { Component, DestroyRef, OnInit, inject } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; -import { Observable, EMPTY } from "rxjs"; +import { EMPTY, Observable } from "rxjs"; import { map, switchMap } from "rxjs/operators"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { - RiskInsightsDataService, CriticalAppsService, - PasswordHealthReportApplicationsResponse, + RiskInsightsDataService, } from "@bitwarden/bit-common/tools/reports/risk-insights"; -import { ApplicationHealthReportDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; +import { + ApplicationHealthReportDetail, + PasswordHealthReportApplicationsResponse, +} from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; // eslint-disable-next-line no-restricted-imports -- used for dependency injection import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts index 321aae165c5..52ec2901031 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts @@ -93,17 +93,16 @@ export class MemberAccessReportComponent implements OnInit { }); }; - edit = async (user: MemberAccessReportView | null): Promise => { + edit = async (user: MemberAccessReportView): Promise => { const dialog = openUserAddEditDialog(this.dialogService, { data: { + kind: "Edit", name: this.userNamePipe.transform(user), organizationId: this.organizationId, - organizationUserId: user != null ? user.userGuid : null, - allOrganizationUserEmails: this.dataSource.data?.map((user) => user.email) ?? [], - usesKeyConnector: user?.usesKeyConnector, + organizationUserId: user.userGuid, + usesKeyConnector: user.usesKeyConnector, isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone, initialTab: MemberDialogTab.Role, - numSeatsUsed: this.dataSource.data.length, }, }); diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-expired.component.ts b/libs/angular/src/auth/components/authentication-timeout.component.ts similarity index 89% rename from libs/angular/src/auth/components/two-factor-auth/two-factor-auth-expired.component.ts rename to libs/angular/src/auth/components/authentication-timeout.component.ts index faa08cf073b..1a5d398a291 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-expired.component.ts +++ b/libs/angular/src/auth/components/authentication-timeout.component.ts @@ -10,7 +10,7 @@ import { ButtonModule } from "@bitwarden/components"; * It provides a button to navigate to the login page. */ @Component({ - selector: "app-two-factor-expired", + selector: "app-authentication-timeout", standalone: true, imports: [CommonModule, JslibModule, ButtonModule, RouterModule], template: ` @@ -22,4 +22,4 @@ import { ButtonModule } from "@bitwarden/components"; `, }) -export class TwoFactorTimeoutComponent {} +export class AuthenticationTimeoutComponent {} diff --git a/libs/angular/src/auth/components/two-factor.component.spec.ts b/libs/angular/src/auth/components/two-factor.component.spec.ts index 5a1903d6671..414aa1dc2a3 100644 --- a/libs/angular/src/auth/components/two-factor.component.spec.ts +++ b/libs/angular/src/auth/components/two-factor.component.spec.ts @@ -86,12 +86,12 @@ describe("TwoFactorComponent", () => { }; let selectedUserDecryptionOptions: BehaviorSubject; - let twoFactorTimeoutSubject: BehaviorSubject; + let authenticationSessionTimeoutSubject: BehaviorSubject; beforeEach(() => { - twoFactorTimeoutSubject = new BehaviorSubject(false); + authenticationSessionTimeoutSubject = new BehaviorSubject(false); mockLoginStrategyService = mock(); - mockLoginStrategyService.twoFactorTimeout$ = twoFactorTimeoutSubject; + mockLoginStrategyService.authenticationSessionTimeout$ = authenticationSessionTimeoutSubject; mockRouter = mock(); mockI18nService = mock(); mockApiService = mock(); @@ -153,7 +153,9 @@ describe("TwoFactorComponent", () => { }), }; - selectedUserDecryptionOptions = new BehaviorSubject(null); + selectedUserDecryptionOptions = new BehaviorSubject( + mockUserDecryptionOpts.withMasterPassword, + ); mockUserDecryptionOptionsService.userDecryptionOptions$ = selectedUserDecryptionOptions; TestBed.configureTestingModule({ @@ -497,8 +499,8 @@ describe("TwoFactorComponent", () => { }); it("navigates to the timeout route when timeout expires", async () => { - twoFactorTimeoutSubject.next(true); + authenticationSessionTimeoutSubject.next(true); - expect(mockRouter.navigate).toHaveBeenCalledWith(["2fa-timeout"]); + expect(mockRouter.navigate).toHaveBeenCalledWith(["authentication-timeout"]); }); }); diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index e2b41ad086d..3b3459f42fb 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -71,7 +71,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected changePasswordRoute = "set-password"; protected forcePasswordResetRoute = "update-temp-password"; protected successRoute = "vault"; - protected twoFactorTimeoutRoute = "2fa-timeout"; + protected twoFactorTimeoutRoute = "authentication-timeout"; get isDuoProvider(): boolean { return ( @@ -104,8 +104,8 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI super(environmentService, i18nService, platformUtilsService, toastService); this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); - // Add subscription to twoFactorTimeout$ and navigate to twoFactorTimeoutRoute if expired - this.loginStrategyService.twoFactorTimeout$ + // Add subscription to authenticationSessionTimeout$ and navigate to twoFactorTimeoutRoute if expired + this.loginStrategyService.authenticationSessionTimeout$ .pipe(takeUntilDestroyed()) .subscribe(async (expired) => { if (!expired) { diff --git a/libs/angular/src/auth/guards/active-auth.guard.spec.ts b/libs/angular/src/auth/guards/active-auth.guard.spec.ts new file mode 100644 index 00000000000..c3417b9d41d --- /dev/null +++ b/libs/angular/src/auth/guards/active-auth.guard.spec.ts @@ -0,0 +1,71 @@ +import { Component } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { Router } from "@angular/router"; +import { RouterTestingModule } from "@angular/router/testing"; +import { MockProxy, mock } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + +import { activeAuthGuard } from "./active-auth.guard"; + +@Component({ template: "" }) +class EmptyComponent {} + +describe("activeAuthGuard", () => { + const setup = (authType: AuthenticationType | null) => { + const loginStrategyService: MockProxy = + mock(); + const currentAuthTypeSubject = new BehaviorSubject(authType); + loginStrategyService.currentAuthType$ = currentAuthTypeSubject; + + const logService: MockProxy = mock(); + + const testBed = TestBed.configureTestingModule({ + imports: [ + RouterTestingModule.withRoutes([ + { path: "", component: EmptyComponent }, + { + path: "protected-route", + component: EmptyComponent, + canActivate: [activeAuthGuard()], + }, + { path: "login", component: EmptyComponent }, + ]), + ], + providers: [ + { provide: LoginStrategyServiceAbstraction, useValue: loginStrategyService }, + { provide: LogService, useValue: logService }, + ], + declarations: [EmptyComponent], + }); + + return { + router: testBed.inject(Router), + logService, + loginStrategyService, + }; + }; + + it("creates the guard", () => { + const { router } = setup(AuthenticationType.Password); + expect(router).toBeTruthy(); + }); + + it("allows access with an active login session", async () => { + const { router } = setup(AuthenticationType.Password); + + await router.navigate(["protected-route"]); + expect(router.url).toBe("/protected-route"); + }); + + it("redirects to login with no active session", async () => { + const { router, logService } = setup(null); + + await router.navigate(["protected-route"]); + expect(router.url).toBe("/login"); + expect(logService.error).toHaveBeenCalledWith("No active login session found."); + }); +}); diff --git a/libs/angular/src/auth/guards/active-auth.guard.ts b/libs/angular/src/auth/guards/active-auth.guard.ts new file mode 100644 index 00000000000..56213bbd979 --- /dev/null +++ b/libs/angular/src/auth/guards/active-auth.guard.ts @@ -0,0 +1,28 @@ +import { inject } from "@angular/core"; +import { CanActivateFn, Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; + +import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + +/** + * Guard that ensures there is an active login session before allowing access + * to the new device verification route. + * If not, redirects to login. + */ +export function activeAuthGuard(): CanActivateFn { + return async () => { + const loginStrategyService = inject(LoginStrategyServiceAbstraction); + const logService = inject(LogService); + const router = inject(Router); + + // Check if we have a valid login session + const authType = await firstValueFrom(loginStrategyService.currentAuthType$); + if (authType === null) { + logService.error("No active login session found."); + return router.createUrlTree(["/login"]); + } + + return true; + }; +} diff --git a/libs/angular/src/auth/guards/index.ts b/libs/angular/src/auth/guards/index.ts index 1760a870b3a..026848c4b08 100644 --- a/libs/angular/src/auth/guards/index.ts +++ b/libs/angular/src/auth/guards/index.ts @@ -1,4 +1,5 @@ export * from "./auth.guard"; +export * from "./active-auth.guard"; export * from "./lock.guard"; export * from "./redirect.guard"; export * from "./tde-decryption-required.guard"; diff --git a/libs/angular/src/auth/guards/unauth.guard.spec.ts b/libs/angular/src/auth/guards/unauth.guard.spec.ts index 6d8619f4d43..ec36b146a03 100644 --- a/libs/angular/src/auth/guards/unauth.guard.spec.ts +++ b/libs/angular/src/auth/guards/unauth.guard.spec.ts @@ -5,17 +5,48 @@ import { MockProxy, mock } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { EmptyComponent } from "@bitwarden/angular/platform/guard/feature-flag.guard.spec"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { KeyService } from "@bitwarden/key-management"; import { unauthGuardFn } from "./unauth.guard"; describe("UnauthGuard", () => { - const setup = (authStatus: AuthenticationStatus) => { + const activeUser: Account = { + id: "fake_user_id" as UserId, + email: "test@email.com", + emailVerified: true, + name: "Test User", + }; + + const setup = ( + activeUser: Account | null, + authStatus: AuthenticationStatus | null = null, + tdeEnabled: boolean = false, + everHadUserKey: boolean = false, + ) => { + const accountService: MockProxy = mock(); const authService: MockProxy = mock(); - authService.getAuthStatus.mockResolvedValue(authStatus); - const activeAccountStatusObservable = new BehaviorSubject(authStatus); - authService.activeAccountStatus$ = activeAccountStatusObservable; + const keyService: MockProxy = mock(); + const deviceTrustService: MockProxy = + mock(); + const logService: MockProxy = mock(); + + accountService.activeAccount$ = new BehaviorSubject(activeUser); + + if (authStatus !== null) { + const activeAccountStatusObservable = new BehaviorSubject(authStatus); + authService.authStatusFor$.mockReturnValue(activeAccountStatusObservable); + } + + keyService.everHadUserKey$ = new BehaviorSubject(everHadUserKey); + deviceTrustService.supportsDeviceTrustByUserId$.mockReturnValue( + new BehaviorSubject(tdeEnabled), + ); const testBed = TestBed.configureTestingModule({ imports: [ @@ -30,6 +61,7 @@ describe("UnauthGuard", () => { { path: "lock", component: EmptyComponent }, { path: "testhomepage", component: EmptyComponent }, { path: "testlocked", component: EmptyComponent }, + { path: "login-initiated", component: EmptyComponent }, { path: "testOverrides", component: EmptyComponent, @@ -39,7 +71,13 @@ describe("UnauthGuard", () => { }, ]), ], - providers: [{ provide: AuthService, useValue: authService }], + providers: [ + { provide: AccountService, useValue: accountService }, + { provide: AuthService, useValue: authService }, + { provide: KeyService, useValue: keyService }, + { provide: DeviceTrustServiceAbstraction, useValue: deviceTrustService }, + { provide: LogService, useValue: logService }, + ], }); return { @@ -48,40 +86,54 @@ describe("UnauthGuard", () => { }; it("should be created", () => { - const { router } = setup(AuthenticationStatus.LoggedOut); + const { router } = setup(null, AuthenticationStatus.LoggedOut); expect(router).toBeTruthy(); }); it("should redirect to /vault for guarded routes when logged in and unlocked", async () => { - const { router } = setup(AuthenticationStatus.Unlocked); + const { router } = setup(activeUser, AuthenticationStatus.Unlocked); await router.navigateByUrl("unauth-guarded-route"); expect(router.url).toBe("/vault"); }); + it("should allow access to guarded routes when account is null", async () => { + const { router } = setup(null); + + await router.navigateByUrl("unauth-guarded-route"); + expect(router.url).toBe("/unauth-guarded-route"); + }); + it("should allow access to guarded routes when logged out", async () => { - const { router } = setup(AuthenticationStatus.LoggedOut); + const { router } = setup(null, AuthenticationStatus.LoggedOut); await router.navigateByUrl("unauth-guarded-route"); expect(router.url).toBe("/unauth-guarded-route"); }); + it("should redirect to /login-initiated when locked, TDE is enabled, and the user hasn't decrypted yet", async () => { + const { router } = setup(activeUser, AuthenticationStatus.Locked, true, false); + + await router.navigateByUrl("unauth-guarded-route"); + expect(router.url).toBe("/login-initiated"); + }); + it("should redirect to /lock for guarded routes when locked", async () => { - const { router } = setup(AuthenticationStatus.Locked); + const { router } = setup(activeUser, AuthenticationStatus.Locked); await router.navigateByUrl("unauth-guarded-route"); expect(router.url).toBe("/lock"); }); it("should redirect to /testhomepage for guarded routes when testOverrides are provided and the account is unlocked", async () => { - const { router } = setup(AuthenticationStatus.Unlocked); + const { router } = setup(activeUser, AuthenticationStatus.Unlocked); await router.navigateByUrl("testOverrides"); expect(router.url).toBe("/testhomepage"); }); it("should redirect to /testlocked for guarded routes when testOverrides are provided and the account is locked", async () => { - const { router } = setup(AuthenticationStatus.Locked); + const { router } = setup(activeUser, AuthenticationStatus.Locked); await router.navigateByUrl("testOverrides"); expect(router.url).toBe("/testlocked"); diff --git a/libs/angular/src/auth/guards/unauth.guard.ts b/libs/angular/src/auth/guards/unauth.guard.ts index f96668773ef..1ac0eebb458 100644 --- a/libs/angular/src/auth/guards/unauth.guard.ts +++ b/libs/angular/src/auth/guards/unauth.guard.ts @@ -1,9 +1,13 @@ import { inject } from "@angular/core"; -import { CanActivateFn, Router, UrlTree } from "@angular/router"; -import { Observable, map } from "rxjs"; +import { ActivatedRouteSnapshot, CanActivateFn, Router, UrlTree } from "@angular/router"; +import { firstValueFrom } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { KeyService } from "@bitwarden/key-management"; type UnauthRoutes = { homepage: () => string; @@ -15,23 +19,54 @@ const defaultRoutes: UnauthRoutes = { locked: "/lock", }; -function unauthGuard(routes: UnauthRoutes): Observable { +// TODO: PM-17195 - Investigate consolidating unauthGuard and redirectGuard into AuthStatusGuard +async function unauthGuard( + route: ActivatedRouteSnapshot, + routes: UnauthRoutes, +): Promise { + const accountService = inject(AccountService); const authService = inject(AuthService); const router = inject(Router); + const keyService = inject(KeyService); + const deviceTrustService = inject(DeviceTrustServiceAbstraction); + const logService = inject(LogService); - return authService.activeAccountStatus$.pipe( - map((status) => { - if (status == null || status === AuthenticationStatus.LoggedOut) { - return true; - } else if (status === AuthenticationStatus.Locked) { - return router.createUrlTree([routes.locked]); - } else { - return router.createUrlTree([routes.homepage()]); - } - }), + const activeUser = await firstValueFrom(accountService.activeAccount$); + + if (!activeUser) { + return true; + } + + const authStatus = await firstValueFrom(authService.authStatusFor$(activeUser.id)); + + if (authStatus == null || authStatus === AuthenticationStatus.LoggedOut) { + return true; + } + + if (authStatus === AuthenticationStatus.Unlocked) { + return router.createUrlTree([routes.homepage()]); + } + + const tdeEnabled = await firstValueFrom( + deviceTrustService.supportsDeviceTrustByUserId$(activeUser.id), ); + const everHadUserKey = await firstValueFrom(keyService.everHadUserKey$); + + // If locked, TDE is enabled, and the user hasn't decrypted yet, then redirect to the + // login decryption options component. + if (authStatus === AuthenticationStatus.Locked && tdeEnabled && !everHadUserKey) { + logService.info( + "Sending user to TDE decryption options. AuthStatus is %s. TDE support is %s. Ever had user key is %s.", + AuthenticationStatus[authStatus], + tdeEnabled, + everHadUserKey, + ); + return router.createUrlTree(["/login-initiated"]); + } + + return router.createUrlTree([routes.locked]); } export function unauthGuardFn(overrides: Partial = {}): CanActivateFn { - return () => unauthGuard({ ...defaultRoutes, ...overrides }); + return async (route) => unauthGuard(route, { ...defaultRoutes, ...overrides }); } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 766f0a2d411..50095e55400 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -39,6 +39,8 @@ import { DefaultAuthRequestApiService, DefaultLoginSuccessHandlerService, LoginSuccessHandlerService, + PasswordLoginStrategy, + PasswordLoginStrategyData, LoginApprovalComponentServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; @@ -1234,7 +1236,6 @@ const safeProviders: SafeProvider[] = [ deps: [ ApiServiceAbstraction, BillingApiServiceAbstraction, - ConfigService, KeyService, EncryptService, I18nServiceAbstraction, @@ -1436,6 +1437,37 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultLoginSuccessHandlerService, deps: [SyncService, UserAsymmetricKeysRegenerationService], }), + safeProvider({ + provide: PasswordLoginStrategy, + useClass: PasswordLoginStrategy, + deps: [ + PasswordLoginStrategyData, + PasswordStrengthServiceAbstraction, + PolicyServiceAbstraction, + LoginStrategyServiceAbstraction, + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, + KeyService, + EncryptService, + ApiServiceAbstraction, + TokenServiceAbstraction, + AppIdServiceAbstraction, + PlatformUtilsServiceAbstraction, + MessagingServiceAbstraction, + LogService, + StateServiceAbstraction, + TwoFactorServiceAbstraction, + InternalUserDecryptionOptionsServiceAbstraction, + BillingAccountProfileStateService, + VaultTimeoutSettingsServiceAbstraction, + KdfConfigService, + ], + }), + safeProvider({ + provide: PasswordLoginStrategyData, + useClass: PasswordLoginStrategyData, + deps: [], + }), ]; @NgModule({ diff --git a/libs/angular/src/tools/send/add-edit.component.ts b/libs/angular/src/tools/send/add-edit.component.ts index aeee1fa104c..4f7d4b6b600 100644 --- a/libs/angular/src/tools/send/add-edit.component.ts +++ b/libs/angular/src/tools/send/add-edit.component.ts @@ -16,6 +16,7 @@ import { import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -164,9 +165,10 @@ export class AddEditComponent implements OnInit, OnDestroy { } }); - this.policyService - .getAll$(PolicyType.SendOptions) + this.accountService.activeAccount$ .pipe( + getUserId, + switchMap((userId) => this.policyService.getAll$(PolicyType.SendOptions, userId)), map((policies) => policies?.some((p) => p.data.disableHideEmail)), takeUntil(this.destroy$), ) diff --git a/libs/auth/src/angular/icons/device-verification.icon.ts b/libs/auth/src/angular/icons/device-verification.icon.ts new file mode 100644 index 00000000000..b1be4efdfb3 --- /dev/null +++ b/libs/auth/src/angular/icons/device-verification.icon.ts @@ -0,0 +1,18 @@ +import { svgIcon } from "@bitwarden/components"; + +export const DeviceVerificationIcon = svgIcon` + + + + + + + + + + + + + + +`; diff --git a/libs/auth/src/angular/icons/index.ts b/libs/auth/src/angular/icons/index.ts index 0e86ee7fc8e..0ec92d54547 100644 --- a/libs/auth/src/angular/icons/index.ts +++ b/libs/auth/src/angular/icons/index.ts @@ -12,3 +12,4 @@ export * from "./registration-lock-alt.icon"; export * from "./registration-expired-link.icon"; export * from "./sso-key.icon"; export * from "./two-factor-timeout.icon"; +export * from "./device-verification.icon"; diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts index 66111f3e5af..67ab68852b2 100644 --- a/libs/auth/src/angular/index.ts +++ b/libs/auth/src/angular/index.ts @@ -71,3 +71,6 @@ export * from "./self-hosted-env-config-dialog/self-hosted-env-config-dialog.com // login approval export * from "./login-approval/login-approval.component"; export * from "./login-approval/default-login-approval-component.service"; + +// device verification +export * from "./new-device-verification/new-device-verification.component"; diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index f9aaa5d1e05..66fe2503508 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -275,6 +275,12 @@ export class LoginComponent implements OnInit, OnDestroy { return; } + // Redirect to device verification if this is an unknown device + if (authResult.requiresDeviceVerification) { + await this.router.navigate(["device-verification"]); + return; + } + await this.loginSuccessHandlerService.run(authResult.userId); if (authResult.forcePasswordReset != ForceSetPasswordReason.None) { diff --git a/libs/auth/src/angular/new-device-verification/new-device-verification.component.html b/libs/auth/src/angular/new-device-verification/new-device-verification.component.html new file mode 100644 index 00000000000..2f807d32993 --- /dev/null +++ b/libs/auth/src/angular/new-device-verification/new-device-verification.component.html @@ -0,0 +1,36 @@ +
+ + {{ "verificationCode" | i18n }} + + + + + +
+ +
+
diff --git a/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts b/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts new file mode 100644 index 00000000000..6e0f9eec05e --- /dev/null +++ b/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts @@ -0,0 +1,163 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; +import { Router } from "@angular/router"; +import { Subject, takeUntil } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { + AsyncActionsModule, + ButtonModule, + FormFieldModule, + IconButtonModule, + LinkModule, + ToastService, +} from "@bitwarden/components"; + +import { LoginEmailServiceAbstraction } from "../../common/abstractions/login-email.service"; +import { LoginStrategyServiceAbstraction } from "../../common/abstractions/login-strategy.service"; +import { PasswordLoginStrategy } from "../../common/login-strategies/password-login.strategy"; + +/** + * Component for verifying a new device via a one-time password (OTP). + */ +@Component({ + standalone: true, + selector: "app-new-device-verification", + templateUrl: "./new-device-verification.component.html", + imports: [ + CommonModule, + ReactiveFormsModule, + AsyncActionsModule, + JslibModule, + ButtonModule, + FormFieldModule, + IconButtonModule, + LinkModule, + ], +}) +export class NewDeviceVerificationComponent implements OnInit, OnDestroy { + formGroup = this.formBuilder.group({ + code: [ + "", + { + validators: [Validators.required], + updateOn: "change", + }, + ], + }); + + protected disableRequestOTP = false; + private destroy$ = new Subject(); + protected authenticationSessionTimeoutRoute = "/authentication-timeout"; + + constructor( + private router: Router, + private formBuilder: FormBuilder, + private passwordLoginStrategy: PasswordLoginStrategy, + private apiService: ApiService, + private loginStrategyService: LoginStrategyServiceAbstraction, + private logService: LogService, + private toastService: ToastService, + private i18nService: I18nService, + private syncService: SyncService, + private loginEmailService: LoginEmailServiceAbstraction, + ) {} + + async ngOnInit() { + // Redirect to timeout route if session expires + this.loginStrategyService.authenticationSessionTimeout$ + .pipe(takeUntil(this.destroy$)) + .subscribe((expired) => { + if (!expired) { + return; + } + + try { + void this.router.navigate([this.authenticationSessionTimeoutRoute]); + } catch (err) { + this.logService.error( + `Failed to navigate to ${this.authenticationSessionTimeoutRoute} route`, + err, + ); + } + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + /** + * Resends the OTP for device verification. + */ + async resendOTP() { + this.disableRequestOTP = true; + try { + const email = await this.loginStrategyService.getEmail(); + const masterPasswordHash = await this.loginStrategyService.getMasterPasswordHash(); + + if (!email || !masterPasswordHash) { + throw new Error("Missing email or master password hash"); + } + + await this.apiService.send( + "POST", + "/accounts/resend-new-device-otp", + { + email: email, + masterPasswordHash: masterPasswordHash, + }, + false, + false, + ); + } catch (e) { + this.logService.error(e); + } finally { + this.disableRequestOTP = false; + } + } + + /** + * Submits the OTP for device verification. + */ + submit = async (): Promise => { + const codeControl = this.formGroup.get("code"); + if (!codeControl || !codeControl.value) { + return; + } + + try { + const authResult = await this.loginStrategyService.logInNewDeviceVerification( + codeControl.value, + ); + + if (authResult.requiresTwoFactor) { + await this.router.navigate(["/2fa"]); + return; + } + + if (authResult.forcePasswordReset) { + await this.router.navigate(["/update-temp-password"]); + return; + } + + this.loginEmailService.clearValues(); + + await this.syncService.fullSync(true); + + // If verification succeeds, navigate to vault + await this.router.navigate(["/vault"]); + } catch (e) { + this.logService.error(e); + const errorMessage = + (e as any)?.response?.error_description ?? this.i18nService.t("errorOccurred"); + codeControl.setErrors({ serverError: { message: errorMessage } }); + } + }; +} 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 31b3f7db92a..c419e1f427f 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 @@ -16,7 +16,11 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ToastService } from "@bitwarden/components"; -import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "../../../common"; +import { + LoginStrategyServiceAbstraction, + LoginSuccessHandlerService, + PasswordLoginCredentials, +} from "../../../common"; import { AnonLayoutWrapperDataService } from "../../anon-layout/anon-layout-wrapper-data.service"; import { InputPasswordComponent } from "../../input-password/input-password.component"; import { PasswordInputResult } from "../../input-password/password-input-result"; @@ -68,6 +72,7 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { private loginStrategyService: LoginStrategyServiceAbstraction, private logService: LogService, private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, + private loginSuccessHandlerService: LoginSuccessHandlerService, ) {} async ngOnInit() { @@ -189,6 +194,8 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { message: this.i18nService.t("youHaveBeenLoggedIn"), }); + await this.loginSuccessHandlerService.run(authenticationResult.userId); + await this.router.navigate(["/vault"]); } catch (e) { // If login errors, redirect to login page per product. Don't show error diff --git a/libs/auth/src/common/abstractions/login-strategy.service.ts b/libs/auth/src/common/abstractions/login-strategy.service.ts index 1088d6de736..bd725f29024 100644 --- a/libs/auth/src/common/abstractions/login-strategy.service.ts +++ b/libs/auth/src/common/abstractions/login-strategy.service.ts @@ -47,7 +47,6 @@ export abstract class LoginStrategyServiceAbstraction { * Auth Request. Otherwise, it will return null. */ getAuthRequestId: () => Promise; - /** * Sends a token request to the server using the provided credentials. */ @@ -74,7 +73,11 @@ export abstract class LoginStrategyServiceAbstraction { */ makePreloginKey: (masterPassword: string, email: string) => Promise; /** - * Emits true if the two factor session has expired. + * Emits true if the authentication session has expired. + */ + authenticationSessionTimeout$: Observable; + /** + * Sends a token request to the server with the provided device verification OTP. */ - twoFactorTimeout$: Observable; + logInNewDeviceVerification: (deviceVerificationOtp: string) => Promise; } diff --git a/libs/auth/src/common/index.ts b/libs/auth/src/common/index.ts index 43efd7c6387..97909bdc449 100644 --- a/libs/auth/src/common/index.ts +++ b/libs/auth/src/common/index.ts @@ -6,3 +6,4 @@ export * from "./models"; export * from "./types"; export * from "./services"; export * from "./utilities"; +export * from "./login-strategies"; diff --git a/libs/auth/src/common/login-strategies/index.ts b/libs/auth/src/common/login-strategies/index.ts new file mode 100644 index 00000000000..166ef935e08 --- /dev/null +++ b/libs/auth/src/common/login-strategies/index.ts @@ -0,0 +1 @@ +export { PasswordLoginStrategy, PasswordLoginStrategyData } from "./password-login.strategy"; diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index 50443bab0ea..a8208a1e0ad 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -4,6 +4,7 @@ import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -12,6 +13,7 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response"; +import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response"; @@ -76,8 +78,8 @@ const twoFactorToken = "TWO_FACTOR_TOKEN"; const twoFactorRemember = true; export function identityTokenResponseFactory( - masterPasswordPolicyResponse: MasterPasswordPolicyResponse = null, - userDecryptionOptions: IUserDecryptionOptionsServerResponse = null, + masterPasswordPolicyResponse: MasterPasswordPolicyResponse | undefined = undefined, + userDecryptionOptions: IUserDecryptionOptionsServerResponse | undefined = undefined, ) { return new IdentityTokenResponse({ ForcePasswordReset: false, @@ -155,7 +157,7 @@ describe("LoginStrategy", () => { passwordStrengthService, policyService, loginStrategyService, - accountService, + accountService as unknown as AccountService, masterPasswordService, keyService, encryptService, @@ -286,13 +288,16 @@ describe("LoginStrategy", () => { const result = await passwordLoginStrategy.logIn(credentials); - expect(result).toEqual({ - userId: userId, - forcePasswordReset: ForceSetPasswordReason.AdminForcePasswordReset, - resetMasterPassword: true, - twoFactorProviders: null, - captchaSiteKey: "", - } as AuthResult); + const expected = new AuthResult(); + expected.userId = userId; + expected.forcePasswordReset = ForceSetPasswordReason.AdminForcePasswordReset; + expected.resetMasterPassword = true; + expected.twoFactorProviders = {} as Partial< + Record> + >; + expected.captchaSiteKey = ""; + expected.twoFactorProviders = null; + expect(result).toEqual(expected); }); it("rejects login if CAPTCHA is required", async () => { @@ -377,10 +382,11 @@ describe("LoginStrategy", () => { expect(tokenService.clearTwoFactorToken).toHaveBeenCalled(); const expected = new AuthResult(); - expected.twoFactorProviders = { 0: null } as Record< - TwoFactorProviderType, - Record + expected.twoFactorProviders = { 0: null } as unknown as Partial< + Record> >; + expected.email = ""; + expected.ssoEmail2FaSessionToken = undefined; expect(result).toEqual(expected); }); @@ -460,14 +466,19 @@ describe("LoginStrategy", () => { it("sends 2FA token provided by user to server (two-step)", async () => { // Simulate a partially completed login cache = new PasswordLoginStrategyData(); - cache.tokenRequest = new PasswordTokenRequest(email, masterPasswordHash, null, null); + cache.tokenRequest = new PasswordTokenRequest( + email, + masterPasswordHash, + "", + new TokenTwoFactorRequest(), + ); passwordLoginStrategy = new PasswordLoginStrategy( cache, passwordStrengthService, policyService, loginStrategyService, - accountService, + accountService as AccountService, masterPasswordService, keyService, encryptService, @@ -489,7 +500,7 @@ describe("LoginStrategy", () => { await passwordLoginStrategy.logInTwoFactor( new TokenTwoFactorRequest(twoFactorProviderType, twoFactorToken, twoFactorRemember), - null, + "", ); expect(apiService.postIdentityToken).toHaveBeenCalledWith( @@ -503,4 +514,54 @@ describe("LoginStrategy", () => { ); }); }); + + describe("Device verification", () => { + it("processes device verification response", async () => { + const captchaToken = "test-captcha-token"; + const deviceVerificationResponse = new IdentityDeviceVerificationResponse({ + error: "invalid_grant", + error_description: "Device verification required.", + email: "test@bitwarden.com", + deviceVerificationRequest: true, + captchaToken: captchaToken, + }); + + apiService.postIdentityToken.mockResolvedValue(deviceVerificationResponse); + + cache = new PasswordLoginStrategyData(); + cache.tokenRequest = new PasswordTokenRequest( + email, + masterPasswordHash, + "", + new TokenTwoFactorRequest(), + ); + + passwordLoginStrategy = new PasswordLoginStrategy( + cache, + passwordStrengthService, + policyService, + loginStrategyService, + accountService as AccountService, + masterPasswordService, + keyService, + encryptService, + apiService, + tokenService, + appIdService, + platformUtilsService, + messagingService, + logService, + stateService, + twoFactorService, + userDecryptionOptionsService, + billingAccountProfileStateService, + vaultTimeoutSettingsService, + kdfConfigService, + ); + + const result = await passwordLoginStrategy.logIn(credentials); + + expect(result.requiresDeviceVerification).toBe(true); + }); + }); }); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index 25f99f47840..6b1dcfb155c 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -1,6 +1,4 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { BehaviorSubject, filter, firstValueFrom, timeout } from "rxjs"; +import { BehaviorSubject, filter, firstValueFrom, timeout, Observable } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; @@ -18,6 +16,7 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide import { UserApiTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/user-api-token.request"; import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request"; import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response"; +import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; @@ -51,14 +50,19 @@ import { import { UserDecryptionOptions } from "../models/domain/user-decryption-options"; import { CacheData } from "../services/login-strategies/login-strategy.state"; -type IdentityResponse = IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse; +type IdentityResponse = + | IdentityTokenResponse + | IdentityTwoFactorResponse + | IdentityCaptchaResponse + | IdentityDeviceVerificationResponse; export abstract class LoginStrategyData { tokenRequest: | UserApiTokenRequest | PasswordTokenRequest | SsoTokenRequest - | WebAuthnLoginTokenRequest; + | WebAuthnLoginTokenRequest + | undefined; captchaBypassToken?: string; /** User's entered email obtained pre-login. */ @@ -67,6 +71,8 @@ export abstract class LoginStrategyData { export abstract class LoginStrategy { protected abstract cache: BehaviorSubject; + protected sessionTimeoutSubject = new BehaviorSubject(false); + sessionTimeout$: Observable = this.sessionTimeoutSubject.asObservable(); constructor( protected accountService: AccountService, @@ -100,9 +106,12 @@ export abstract class LoginStrategy { async logInTwoFactor( twoFactor: TokenTwoFactorRequest, - captchaResponse: string = null, + captchaResponse: string | null = null, ): Promise { const data = this.cache.value; + if (!data.tokenRequest) { + throw new Error("Token request is undefined"); + } data.tokenRequest.setTwoFactor(twoFactor); this.cache.next(data); const [authResult] = await this.startLogIn(); @@ -113,6 +122,9 @@ export abstract class LoginStrategy { await this.twoFactorService.clearSelectedProvider(); const tokenRequest = this.cache.value.tokenRequest; + if (!tokenRequest) { + throw new Error("Token request is undefined"); + } const response = await this.apiService.postIdentityToken(tokenRequest); if (response instanceof IdentityTwoFactorResponse) { @@ -121,6 +133,8 @@ export abstract class LoginStrategy { return [await this.processCaptchaResponse(response), response]; } else if (response instanceof IdentityTokenResponse) { return [await this.processTokenResponse(response), response]; + } else if (response instanceof IdentityDeviceVerificationResponse) { + return [await this.processDeviceVerificationResponse(response), response]; } throw new Error("Invalid response object."); @@ -176,8 +190,8 @@ export abstract class LoginStrategy { await this.accountService.addAccount(userId, { name: accountInformation.name, - email: accountInformation.email, - emailVerified: accountInformation.email_verified, + email: accountInformation.email ?? "", + emailVerified: accountInformation.email_verified ?? false, }); await this.accountService.switchAccount(userId); @@ -230,7 +244,7 @@ export abstract class LoginStrategy { ); await this.billingAccountProfileStateService.setHasPremium( - accountInformation.premium, + accountInformation.premium ?? false, false, userId, ); @@ -291,6 +305,9 @@ export abstract class LoginStrategy { try { const userKey = await this.keyService.getUserKeyWithLegacySupport(userId); const [publicKey, privateKey] = await this.keyService.makeKeyPair(userKey); + if (!privateKey.encryptedString) { + throw new Error("Failed to create encrypted private key"); + } await this.apiService.postAccountKeys(new KeysRequest(publicKey, privateKey.encryptedString)); return privateKey.encryptedString; } catch (e) { @@ -316,7 +333,8 @@ export abstract class LoginStrategy { await this.twoFactorService.setProviders(response); this.cache.next({ ...this.cache.value, captchaBypassToken: response.captchaToken ?? null }); result.ssoEmail2FaSessionToken = response.ssoEmail2faSessionToken; - result.email = response.email; + + result.email = response.email ?? ""; return result; } @@ -355,4 +373,22 @@ export abstract class LoginStrategy { ), ); } + + /** + * Handles the response from the server when a device verification is required. + * It sets the requiresDeviceVerification flag to true and caches the captcha token if it came back. + * + * @param {IdentityDeviceVerificationResponse} response - The response from the server indicating that device verification is required. + * @returns {Promise} - A promise that resolves to an AuthResult object + */ + protected async processDeviceVerificationResponse( + response: IdentityDeviceVerificationResponse, + ): Promise { + const result = new AuthResult(); + result.requiresDeviceVerification = true; + + // Extend cached data with captcha bypass token if it came back. + this.cache.next({ ...this.cache.value, captchaBypassToken: response.captchaToken ?? null }); + return result; + } } diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts index 4ee4fcaeb38..d572710a2fd 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts @@ -276,4 +276,24 @@ describe("PasswordLoginStrategy", () => { ); expect(secondResult.forcePasswordReset).toEqual(ForceSetPasswordReason.WeakMasterPassword); }); + + it("handles new device verification login with OTP", async () => { + const deviceVerificationOtp = "123456"; + const tokenResponse = identityTokenResponseFactory(); + apiService.postIdentityToken.mockResolvedValueOnce(tokenResponse); + tokenService.decodeAccessToken.mockResolvedValue({ sub: userId }); + + await passwordLoginStrategy.logIn(credentials); + + const result = await passwordLoginStrategy.logInNewDeviceVerification(deviceVerificationOtp); + + expect(apiService.postIdentityToken).toHaveBeenCalledWith( + expect.objectContaining({ + newDeviceOtp: deviceVerificationOtp, + }), + ); + expect(result.forcePasswordReset).toBe(ForceSetPasswordReason.None); + expect(result.resetMasterPassword).toBe(false); + expect(result.userId).toBe(userId); + }); }); diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index c496b7c9674..f0a8d40f914 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -10,6 +10,7 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response"; +import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { HashPurpose } from "@bitwarden/common/platform/enums"; @@ -208,9 +209,12 @@ export class PasswordLoginStrategy extends LoginStrategy { } private getMasterPasswordPolicyOptionsFromResponse( - response: IdentityTokenResponse | IdentityTwoFactorResponse, + response: + | IdentityTokenResponse + | IdentityTwoFactorResponse + | IdentityDeviceVerificationResponse, ): MasterPasswordPolicyOptions { - if (response == null) { + if (response == null || response instanceof IdentityDeviceVerificationResponse) { return null; } return MasterPasswordPolicyOptions.fromResponse(response.masterPasswordPolicy); @@ -233,4 +237,13 @@ export class PasswordLoginStrategy extends LoginStrategy { password: this.cache.value, }; } + + async logInNewDeviceVerification(deviceVerificationOtp: string): Promise { + const data = this.cache.value; + data.tokenRequest.newDeviceOtp = deviceVerificationOtp; + this.cache.next(data); + + const [authResult] = await this.startLogIn(); + return authResult; + } } diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts index 5fcbefbef2f..3b03e8754bc 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts @@ -321,4 +321,67 @@ describe("LoginStrategyService", () => { `PBKDF2 iterations must be at least ${PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN}, but was ${PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN - 1}; possible pre-login downgrade attack detected.`, ); }); + + it("returns an AuthResult on successful new device verification", async () => { + const credentials = new PasswordLoginCredentials("EMAIL", "MASTER_PASSWORD"); + const deviceVerificationOtp = "123456"; + + // Setup initial login and device verification response + apiService.postPrelogin.mockResolvedValue( + new PreloginResponse({ + Kdf: KdfType.Argon2id, + KdfIterations: 2, + KdfMemory: 16, + KdfParallelism: 1, + }), + ); + + apiService.postIdentityToken.mockResolvedValueOnce( + new IdentityTwoFactorResponse({ + TwoFactorProviders: ["0"], + TwoFactorProviders2: { 0: null }, + error: "invalid_grant", + error_description: "Two factor required.", + email: undefined, + ssoEmail2faSessionToken: undefined, + }), + ); + + await sut.logIn(credentials); + + // Successful device verification login + apiService.postIdentityToken.mockResolvedValueOnce( + new IdentityTokenResponse({ + ForcePasswordReset: false, + Kdf: KdfType.Argon2id, + KdfIterations: 2, + KdfMemory: 16, + KdfParallelism: 1, + Key: "KEY", + PrivateKey: "PRIVATE_KEY", + ResetMasterPassword: false, + access_token: "ACCESS_TOKEN", + expires_in: 3600, + refresh_token: "REFRESH_TOKEN", + scope: "api offline_access", + token_type: "Bearer", + }), + ); + + tokenService.decodeAccessToken.calledWith("ACCESS_TOKEN").mockResolvedValue({ + sub: "USER_ID", + name: "NAME", + email: "EMAIL", + premium: false, + }); + + const result = await sut.logInNewDeviceVerification(deviceVerificationOtp); + + expect(result).toBeInstanceOf(AuthResult); + expect(apiService.postIdentityToken).toHaveBeenCalledWith( + expect.objectContaining({ + newDeviceOtp: deviceVerificationOtp, + }), + ); + }); }); diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 57a653b205e..e3a20fcfe72 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { combineLatestWith, distinctUntilChanged, @@ -15,6 +13,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -35,9 +34,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { MasterKey } from "@bitwarden/common/types/key"; import { @@ -51,12 +47,24 @@ import { import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction } from "../../abstractions"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction"; -import { AuthRequestLoginStrategy } from "../../login-strategies/auth-request-login.strategy"; +import { + AuthRequestLoginStrategy, + AuthRequestLoginStrategyData, +} from "../../login-strategies/auth-request-login.strategy"; import { LoginStrategy } from "../../login-strategies/login.strategy"; -import { PasswordLoginStrategy } from "../../login-strategies/password-login.strategy"; -import { SsoLoginStrategy } from "../../login-strategies/sso-login.strategy"; -import { UserApiLoginStrategy } from "../../login-strategies/user-api-login.strategy"; -import { WebAuthnLoginStrategy } from "../../login-strategies/webauthn-login.strategy"; +import { + PasswordLoginStrategy, + PasswordLoginStrategyData, +} from "../../login-strategies/password-login.strategy"; +import { SsoLoginStrategy, SsoLoginStrategyData } from "../../login-strategies/sso-login.strategy"; +import { + UserApiLoginStrategy, + UserApiLoginStrategyData, +} from "../../login-strategies/user-api-login.strategy"; +import { + WebAuthnLoginStrategy, + WebAuthnLoginStrategyData, +} from "../../login-strategies/webauthn-login.strategy"; import { UserApiLoginCredentials, PasswordLoginCredentials, @@ -76,14 +84,15 @@ import { const sessionTimeoutLength = 5 * 60 * 1000; // 5 minutes export class LoginStrategyService implements LoginStrategyServiceAbstraction { - private sessionTimeoutSubscription: Subscription; + private sessionTimeoutSubscription: Subscription | undefined; private currentAuthnTypeState: GlobalState; private loginStrategyCacheState: GlobalState; private loginStrategyCacheExpirationState: GlobalState; - private authRequestPushNotificationState: GlobalState; - private twoFactorTimeoutSubject = new BehaviorSubject(false); + private authRequestPushNotificationState: GlobalState; + private authenticationTimeoutSubject = new BehaviorSubject(false); - twoFactorTimeout$: Observable = this.twoFactorTimeoutSubject.asObservable(); + authenticationSessionTimeout$: Observable = + this.authenticationTimeoutSubject.asObservable(); private loginStrategy$: Observable< | UserApiLoginStrategy @@ -132,7 +141,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.taskSchedulerService.registerTaskHandler( ScheduledTaskNames.loginStrategySessionTimeout, async () => { - this.twoFactorTimeoutSubject.next(true); + this.authenticationTimeoutSubject.next(true); try { await this.clearCache(); } catch (e) { @@ -153,7 +162,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async getEmail(): Promise { const strategy = await firstValueFrom(this.loginStrategy$); - if ("email$" in strategy) { + if (strategy && "email$" in strategy) { return await firstValueFrom(strategy.email$); } return null; @@ -162,7 +171,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async getMasterPasswordHash(): Promise { const strategy = await firstValueFrom(this.loginStrategy$); - if ("serverMasterKeyHash$" in strategy) { + if (strategy && "serverMasterKeyHash$" in strategy) { return await firstValueFrom(strategy.serverMasterKeyHash$); } return null; @@ -171,7 +180,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async getSsoEmail2FaSessionToken(): Promise { const strategy = await firstValueFrom(this.loginStrategy$); - if ("ssoEmail2FaSessionToken$" in strategy) { + if (strategy && "ssoEmail2FaSessionToken$" in strategy) { return await firstValueFrom(strategy.ssoEmail2FaSessionToken$); } return null; @@ -180,7 +189,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async getAccessCode(): Promise { const strategy = await firstValueFrom(this.loginStrategy$); - if ("accessCode$" in strategy) { + if (strategy && "accessCode$" in strategy) { return await firstValueFrom(strategy.accessCode$); } return null; @@ -189,7 +198,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async getAuthRequestId(): Promise { const strategy = await firstValueFrom(this.loginStrategy$); - if ("authRequestId$" in strategy) { + if (strategy && "authRequestId$" in strategy) { return await firstValueFrom(strategy.authRequestId$); } return null; @@ -204,7 +213,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { | WebAuthnLoginCredentials, ): Promise { await this.clearCache(); - this.twoFactorTimeoutSubject.next(false); + this.authenticationTimeoutSubject.next(false); await this.currentAuthnTypeState.update((_) => credentials.type); @@ -217,16 +226,19 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { // If the popup uses its own instance of this service, this can be removed. const ownedCredentials = { ...credentials }; - const result = await strategy.logIn(ownedCredentials as any); + const result = await strategy?.logIn(ownedCredentials as any); - if (result != null && !result.requiresTwoFactor) { + if (result != null && !result.requiresTwoFactor && !result.requiresDeviceVerification) { await this.clearCache(); } else { - // Cache the strategy data so we can attempt again later with 2fa. Cache supports different contexts - await this.loginStrategyCacheState.update((_) => strategy.exportCache()); + // Cache the strategy data so we can attempt again later with 2fa or device verification + await this.loginStrategyCacheState.update((_) => strategy?.exportCache() ?? null); await this.startSessionTimeout(); } + if (!result) { + throw new Error("No auth result returned"); + } return result; } @@ -260,9 +272,46 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { } } + /** + * Sends a token request to the server with the provided device verification OTP. + * Returns an error if no session data is found or if the current login strategy does not support device verification. + * @param deviceVerificationOtp The OTP to send to the server for device verification. + * @returns The result of the token request. + */ + async logInNewDeviceVerification(deviceVerificationOtp: string): Promise { + if (!(await this.isSessionValid())) { + throw new Error(this.i18nService.t("sessionTimeout")); + } + + const strategy = await firstValueFrom(this.loginStrategy$); + if (strategy == null) { + throw new Error("No login strategy found."); + } + + if (!("logInNewDeviceVerification" in strategy)) { + throw new Error("Current login strategy does not support device verification."); + } + + try { + const result = await strategy.logInNewDeviceVerification(deviceVerificationOtp); + + // Only clear cache if device verification succeeds + if (result !== null && !result.requiresDeviceVerification) { + await this.clearCache(); + } + return result; + } catch (e) { + // Clear the cache if there is an unhandled client-side error + if (!(e instanceof ErrorResponse)) { + await this.clearCache(); + } + throw e; + } + } + async makePreloginKey(masterPassword: string, email: string): Promise { email = email.trim().toLowerCase(); - let kdfConfig: KdfConfig = null; + let kdfConfig: KdfConfig | undefined; try { const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email)); if (preloginResponse != null) { @@ -275,12 +324,15 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { preloginResponse.kdfParallelism, ); } - } catch (e) { + } catch (e: any) { if (e == null || e.statusCode !== 404) { throw e; } } + if (!kdfConfig) { + throw new Error("KDF config is required"); + } kdfConfig.validateKdfConfigForPrelogin(); return await this.keyService.makeMasterKey(masterPassword, email, kdfConfig); @@ -289,7 +341,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { private async clearCache(): Promise { await this.currentAuthnTypeState.update((_) => null); await this.loginStrategyCacheState.update((_) => null); - this.twoFactorTimeoutSubject.next(false); + this.authenticationTimeoutSubject.next(false); await this.clearSessionTimeout(); } @@ -360,7 +412,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { switch (strategy) { case AuthenticationType.Password: return new PasswordLoginStrategy( - data?.password, + data?.password ?? new PasswordLoginStrategyData(), this.passwordStrengthService, this.policyService, this, @@ -368,7 +420,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { ); case AuthenticationType.Sso: return new SsoLoginStrategy( - data?.sso, + data?.sso ?? new SsoLoginStrategyData(), this.keyConnectorService, this.deviceTrustService, this.authRequestService, @@ -377,19 +429,22 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { ); case AuthenticationType.UserApiKey: return new UserApiLoginStrategy( - data?.userApiKey, + data?.userApiKey ?? new UserApiLoginStrategyData(), this.environmentService, this.keyConnectorService, ...sharedDeps, ); case AuthenticationType.AuthRequest: return new AuthRequestLoginStrategy( - data?.authRequest, + data?.authRequest ?? new AuthRequestLoginStrategyData(), this.deviceTrustService, ...sharedDeps, ); case AuthenticationType.WebAuthn: - return new WebAuthnLoginStrategy(data?.webAuthn, ...sharedDeps); + return new WebAuthnLoginStrategy( + data?.webAuthn ?? new WebAuthnLoginStrategyData(), + ...sharedDeps, + ); } }), ); diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index daccc4bd16e..5bd2221860b 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -70,6 +70,7 @@ import { ApiKeyResponse } from "../auth/models/response/api-key.response"; import { AuthRequestResponse } from "../auth/models/response/auth-request.response"; import { DeviceVerificationResponse } from "../auth/models/response/device-verification.response"; import { IdentityCaptchaResponse } from "../auth/models/response/identity-captcha.response"; +import { IdentityDeviceVerificationResponse } from "../auth/models/response/identity-device-verification.response"; import { IdentityTokenResponse } from "../auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response"; import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response"; @@ -151,7 +152,12 @@ export abstract class ApiService { | SsoTokenRequest | UserApiTokenRequest | WebAuthnLoginTokenRequest, - ) => Promise; + ) => Promise< + | IdentityTokenResponse + | IdentityTwoFactorResponse + | IdentityCaptchaResponse + | IdentityDeviceVerificationResponse + >; refreshIdentityToken: () => Promise; getProfile: () => Promise; diff --git a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts index bed341115ee..4280756326c 100644 --- a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts @@ -30,7 +30,7 @@ export abstract class PolicyService { * A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner). * @param policyType the {@link PolicyType} to search for */ - getAll$: (policyType: PolicyType, userId?: UserId) => Observable; + getAll$: (policyType: PolicyType, userId: UserId) => Observable; /** * All {@link Policy} objects for the specified user (from sync data). diff --git a/libs/common/src/admin-console/services/policy/policy.service.spec.ts b/libs/common/src/admin-console/services/policy/policy.service.spec.ts index f0ebfddf66e..12b57f1b4f7 100644 --- a/libs/common/src/admin-console/services/policy/policy.service.spec.ts +++ b/libs/common/src/admin-console/services/policy/policy.service.spec.ts @@ -2,7 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; -import { FakeActiveUserState } from "../../../../spec/fake-state"; +import { FakeActiveUserState, FakeSingleUserState } from "../../../../spec/fake-state"; import { OrganizationUserStatusType, OrganizationUserType, @@ -24,6 +24,7 @@ describe("PolicyService", () => { let stateProvider: FakeStateProvider; let organizationService: MockProxy; let activeUserState: FakeActiveUserState>; + let singleUserState: FakeSingleUserState>; let policyService: PolicyService; @@ -33,6 +34,7 @@ describe("PolicyService", () => { organizationService = mock(); activeUserState = stateProvider.activeUser.getFake(POLICIES); + singleUserState = stateProvider.singleUser.getFake(activeUserState.userId, POLICIES); const organizations$ = of([ // User @@ -295,7 +297,7 @@ describe("PolicyService", () => { describe("getAll$", () => { it("returns the specified PolicyTypes", async () => { - activeUserState.nextState( + singleUserState.nextState( arrayToRecord([ policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true), policyData("policy2", "org1", PolicyType.ActivateAutofill, true), @@ -305,7 +307,7 @@ describe("PolicyService", () => { ); const result = await firstValueFrom( - policyService.getAll$(PolicyType.DisablePersonalVaultExport), + policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId), ); expect(result).toEqual([ @@ -331,7 +333,7 @@ describe("PolicyService", () => { }); it("does not return disabled policies", async () => { - activeUserState.nextState( + singleUserState.nextState( arrayToRecord([ policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true), policyData("policy2", "org1", PolicyType.ActivateAutofill, true), @@ -341,7 +343,7 @@ describe("PolicyService", () => { ); const result = await firstValueFrom( - policyService.getAll$(PolicyType.DisablePersonalVaultExport), + policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId), ); expect(result).toEqual([ @@ -361,7 +363,7 @@ describe("PolicyService", () => { }); it("does not return policies that do not apply to the user because the user's role is exempt", async () => { - activeUserState.nextState( + singleUserState.nextState( arrayToRecord([ policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true), policyData("policy2", "org1", PolicyType.ActivateAutofill, true), @@ -371,7 +373,7 @@ describe("PolicyService", () => { ); const result = await firstValueFrom( - policyService.getAll$(PolicyType.DisablePersonalVaultExport), + policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId), ); expect(result).toEqual([ @@ -391,7 +393,7 @@ describe("PolicyService", () => { }); it("does not return policies for organizations that do not use policies", async () => { - activeUserState.nextState( + singleUserState.nextState( arrayToRecord([ policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true), policyData("policy2", "org1", PolicyType.ActivateAutofill, true), @@ -401,7 +403,7 @@ describe("PolicyService", () => { ); const result = await firstValueFrom( - policyService.getAll$(PolicyType.DisablePersonalVaultExport), + policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId), ); expect(result).toEqual([ diff --git a/libs/common/src/admin-console/services/policy/policy.service.ts b/libs/common/src/admin-console/services/policy/policy.service.ts index bf99b9ce721..3378d2021ef 100644 --- a/libs/common/src/admin-console/services/policy/policy.service.ts +++ b/libs/common/src/admin-console/services/policy/policy.service.ts @@ -51,7 +51,7 @@ export class PolicyService implements InternalPolicyServiceAbstraction { ); } - getAll$(policyType: PolicyType, userId?: UserId) { + getAll$(policyType: PolicyType, userId: UserId) { const filteredPolicies$ = this.stateProvider.getUserState$(POLICIES, userId).pipe( map((policyData) => policyRecordToArray(policyData)), map((policies) => policies.filter((p) => p.type === policyType)), diff --git a/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts b/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts index 13963b03bea..24a5d4e8413 100644 --- a/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts @@ -9,7 +9,18 @@ import { DeviceKey, UserKey } from "../../types/key"; import { DeviceResponse } from "./devices/responses/device.response"; export abstract class DeviceTrustServiceAbstraction { + /** + * @deprecated - use supportsDeviceTrustByUserId instead as active user state is being deprecated + * by Platform + * @description Checks if the device trust feature is supported for the active user. + */ supportsDeviceTrust$: Observable; + + /** + * @description Checks if the device trust feature is supported for the given user. + */ + supportsDeviceTrustByUserId$: (userId: UserId) => Observable; + /** * @description Retrieves the users choice to trust the device which can only happen after decryption * Note: this value should only be used once and then reset diff --git a/libs/common/src/auth/models/domain/auth-result.ts b/libs/common/src/auth/models/domain/auth-result.ts index 1c176c2b84b..fdc8c963a1b 100644 --- a/libs/common/src/auth/models/domain/auth-result.ts +++ b/libs/common/src/auth/models/domain/auth-result.ts @@ -22,6 +22,7 @@ export class AuthResult { ssoEmail2FaSessionToken?: string; email: string; requiresEncryptionKeyMigration: boolean; + requiresDeviceVerification: boolean; get requiresCaptcha() { return !Utils.isNullOrWhitespace(this.captchaSiteKey); diff --git a/libs/common/src/auth/models/request/identity-token/password-token.request.ts b/libs/common/src/auth/models/request/identity-token/password-token.request.ts index 456e058a234..3fe466e143b 100644 --- a/libs/common/src/auth/models/request/identity-token/password-token.request.ts +++ b/libs/common/src/auth/models/request/identity-token/password-token.request.ts @@ -13,6 +13,7 @@ export class PasswordTokenRequest extends TokenRequest implements CaptchaProtect public captchaResponse: string, protected twoFactor: TokenTwoFactorRequest, device?: DeviceRequest, + public newDeviceOtp?: string, ) { super(twoFactor, device); } @@ -28,6 +29,10 @@ export class PasswordTokenRequest extends TokenRequest implements CaptchaProtect obj.captchaResponse = this.captchaResponse; } + if (this.newDeviceOtp) { + obj.newDeviceOtp = this.newDeviceOtp; + } + return obj; } diff --git a/libs/common/src/auth/models/response/identity-device-verification.response.ts b/libs/common/src/auth/models/response/identity-device-verification.response.ts new file mode 100644 index 00000000000..b45f47e99e1 --- /dev/null +++ b/libs/common/src/auth/models/response/identity-device-verification.response.ts @@ -0,0 +1,13 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +export class IdentityDeviceVerificationResponse extends BaseResponse { + deviceVerified: boolean; + captchaToken: string; + + constructor(response: any) { + super(response); + this.deviceVerified = this.getResponseProperty("DeviceVerified") ?? false; + + this.captchaToken = this.getResponseProperty("CaptchaBypassToken"); + } +} diff --git a/libs/common/src/auth/models/response/identity-response.ts b/libs/common/src/auth/models/response/identity-response.ts new file mode 100644 index 00000000000..26503a9cc2f --- /dev/null +++ b/libs/common/src/auth/models/response/identity-response.ts @@ -0,0 +1,8 @@ +import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response"; +import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; +import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; + +export type IdentityResponse = + | IdentityTokenResponse + | IdentityTwoFactorResponse + | IdentityDeviceVerificationResponse; diff --git a/libs/common/src/auth/services/device-trust.service.implementation.ts b/libs/common/src/auth/services/device-trust.service.implementation.ts index a94c8b6e422..15c12b7a39a 100644 --- a/libs/common/src/auth/services/device-trust.service.implementation.ts +++ b/libs/common/src/auth/services/device-trust.service.implementation.ts @@ -81,7 +81,17 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { private configService: ConfigService, ) { this.supportsDeviceTrust$ = this.userDecryptionOptionsService.userDecryptionOptions$.pipe( - map((options) => options?.trustedDeviceOption != null ?? false), + map((options) => { + return options?.trustedDeviceOption != null ?? false; + }), + ); + } + + supportsDeviceTrustByUserId$(userId: UserId): Observable { + return this.userDecryptionOptionsService.userDecryptionOptionsById$(userId).pipe( + map((options) => { + return options?.trustedDeviceOption != null ?? false; + }), ); } diff --git a/libs/common/src/auth/services/device-trust.service.spec.ts b/libs/common/src/auth/services/device-trust.service.spec.ts index 943653e3129..9f344e203c9 100644 --- a/libs/common/src/auth/services/device-trust.service.spec.ts +++ b/libs/common/src/auth/services/device-trust.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 { matches, mock } from "jest-mock-extended"; -import { BehaviorSubject, of } from "rxjs"; +import { BehaviorSubject, firstValueFrom, of } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; @@ -74,17 +76,56 @@ describe("deviceTrustService", () => { userId: mockUserId, }; + let userDecryptionOptions: UserDecryptionOptions; + beforeEach(() => { jest.clearAllMocks(); const supportsSecureStorage = false; // default to false; tests will override as needed // By default all the tests will have a mocked active user in state provider. deviceTrustService = createDeviceTrustService(mockUserId, supportsSecureStorage); + + userDecryptionOptions = new UserDecryptionOptions(); }); it("instantiates", () => { expect(deviceTrustService).not.toBeFalsy(); }); + describe("supportsDeviceTrustByUserId$", () => { + it("returns true when the user has a non-null trusted device decryption option", async () => { + // Arrange + userDecryptionOptions.trustedDeviceOption = { + hasAdminApproval: false, + hasLoginApprovingDevice: false, + hasManageResetPasswordPermission: false, + isTdeOffboarding: false, + }; + + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + new BehaviorSubject(userDecryptionOptions), + ); + + const result = await firstValueFrom( + deviceTrustService.supportsDeviceTrustByUserId$(mockUserId), + ); + expect(result).toBe(true); + }); + + it("returns false when the user has a null trusted device decryption option", async () => { + // Arrange + userDecryptionOptions.trustedDeviceOption = null; + + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + new BehaviorSubject(userDecryptionOptions), + ); + + const result = await firstValueFrom( + deviceTrustService.supportsDeviceTrustByUserId$(mockUserId), + ); + expect(result).toBe(false); + }); + }); + describe("User Trust Device Choice For Decryption", () => { describe("getShouldTrustDevice", () => { it("gets the user trust device choice for decryption", async () => { diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index 1e68488ac98..69309014fac 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -4,7 +4,6 @@ import { OrganizationResponse } from "../../admin-console/models/response/organization.response"; import { InitiationPath } from "../../models/request/reference-event.request"; import { PaymentMethodType, PlanType } from "../enums"; -import { BillingSourceResponse } from "../models/response/billing.response"; import { PaymentSourceResponse } from "../models/response/payment-source.response"; export type OrganizationInformation = { @@ -46,9 +45,7 @@ export type SubscriptionInformation = { }; export abstract class OrganizationBillingServiceAbstraction { - getPaymentSource: ( - organizationId: string, - ) => Promise; + getPaymentSource: (organizationId: string) => Promise; purchaseSubscription: (subscription: SubscriptionInformation) => Promise; diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index da1a1666ff0..e61b092d677 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -7,8 +7,6 @@ import { OrganizationApiServiceAbstraction as OrganizationApiService } from "../ import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; import { OrganizationKeysRequest } from "../../admin-console/models/request/organization-keys.request"; import { OrganizationResponse } from "../../admin-console/models/response/organization.response"; -import { FeatureFlag } from "../../enums/feature-flag.enum"; -import { ConfigService } from "../../platform/abstractions/config/config.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { EncString } from "../../platform/models/domain/enc-string"; @@ -24,7 +22,6 @@ import { } from "../abstractions"; import { PlanType } from "../enums"; import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request"; -import { BillingSourceResponse } from "../models/response/billing.response"; import { PaymentSourceResponse } from "../models/response/payment-source.response"; interface OrganizationKeys { @@ -38,7 +35,6 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs constructor( private apiService: ApiService, private billingApiService: BillingApiServiceAbstraction, - private configService: ConfigService, private keyService: KeyService, private encryptService: EncryptService, private i18nService: I18nService, @@ -46,21 +42,9 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs private syncService: SyncService, ) {} - async getPaymentSource( - organizationId: string, - ): Promise { - const deprecateStripeSourcesAPI = await this.configService.getFeatureFlag( - FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - ); - - if (deprecateStripeSourcesAPI) { - const paymentMethod = - await this.billingApiService.getOrganizationPaymentMethod(organizationId); - return paymentMethod.paymentSource; - } else { - const billing = await this.organizationApiService.getBilling(organizationId); - return billing.paymentSource; - } + async getPaymentSource(organizationId: string): Promise { + const paymentMethod = await this.billingApiService.getOrganizationPaymentMethod(organizationId); + return paymentMethod.paymentSource; } async purchaseSubscription(subscription: SubscriptionInformation): Promise { diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index b321199d5e5..a988bdbf6a7 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -34,7 +34,6 @@ export enum FeatureFlag { UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh", SSHKeyVaultItem = "ssh-key-vault-item", SSHAgent = "ssh-agent", - AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api", CipherKeyEncryption = "cipher-key-encryption", PM11901_RefactorSelfHostingLicenseUploader = "PM-11901-refactor-self-hosting-license-uploader", CriticalApps = "pm-14466-risk-insights-critical-application", @@ -47,6 +46,7 @@ export enum FeatureFlag { PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form", PrivateKeyRegeneration = "pm-12241-private-key-regeneration", ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs", + NewDeviceVerification = "new-device-verification", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -91,7 +91,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE, [FeatureFlag.SSHKeyVaultItem]: FALSE, [FeatureFlag.SSHAgent]: FALSE, - [FeatureFlag.AC2476_DeprecateStripeSourcesAPI]: FALSE, [FeatureFlag.CipherKeyEncryption]: FALSE, [FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader]: FALSE, [FeatureFlag.CriticalApps]: FALSE, @@ -104,6 +103,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE, [FeatureFlag.PrivateKeyRegeneration]: FALSE, [FeatureFlag.ResellerManagedOrgAlert]: FALSE, + [FeatureFlag.NewDeviceVerification]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; diff --git a/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts b/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts index d684561dacd..6a1b7b67b42 100644 --- a/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts +++ b/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts @@ -4,6 +4,10 @@ import type { BitwardenClient } from "@bitwarden/sdk-internal"; * Factory for creating SDK clients. */ export abstract class SdkClientFactory { + /** + * Creates a new BitwardenClient. Assumes the SDK is already loaded. + * @param args Bitwarden client constructor parameters + */ abstract createSdkClient( ...args: ConstructorParameters ): Promise; diff --git a/libs/common/src/platform/abstractions/sdk/sdk-load.service.ts b/libs/common/src/platform/abstractions/sdk/sdk-load.service.ts new file mode 100644 index 00000000000..16482e797b2 --- /dev/null +++ b/libs/common/src/platform/abstractions/sdk/sdk-load.service.ts @@ -0,0 +1,3 @@ +export abstract class SdkLoadService { + abstract load(): Promise; +} diff --git a/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts b/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts index 8e99af2efed..fc55cc83ac8 100644 --- a/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts +++ b/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts @@ -1,19 +1,19 @@ import * as sdk from "@bitwarden/sdk-internal"; -import * as module from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"; import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; /** - * Directly imports the Bitwarden SDK and initializes it. - * - * **Warning**: This requires WASM support and will fail if the environment does not support it. + * Default SDK client factory. */ export class DefaultSdkClientFactory implements SdkClientFactory { + /** + * Initializes a Bitwarden client. Assumes the SDK is already loaded. + * @param args Bitwarden client constructor parameters + * @returns A BitwardenClient + */ async createSdkClient( ...args: ConstructorParameters ): Promise { - (sdk as any).init(module); - return Promise.resolve(new sdk.BitwardenClient(...args)); } } diff --git a/libs/common/src/platform/services/sdk/default-sdk-load.service.ts b/libs/common/src/platform/services/sdk/default-sdk-load.service.ts new file mode 100644 index 00000000000..eff641f0351 --- /dev/null +++ b/libs/common/src/platform/services/sdk/default-sdk-load.service.ts @@ -0,0 +1,15 @@ +import * as sdk from "@bitwarden/sdk-internal"; +import * as bitwardenModule from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"; + +import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service"; + +/** + * Directly imports the Bitwarden SDK and initializes it. + * + * **Warning**: This requires WASM support and will fail if the environment does not support it. + */ +export class DefaultSdkLoadService implements SdkLoadService { + async load(): Promise { + (sdk as any).init(bitwardenModule); + } +} diff --git a/libs/common/src/platform/services/sdk/noop-sdk-load.service.ts b/libs/common/src/platform/services/sdk/noop-sdk-load.service.ts new file mode 100644 index 00000000000..60dac4f21f1 --- /dev/null +++ b/libs/common/src/platform/services/sdk/noop-sdk-load.service.ts @@ -0,0 +1,7 @@ +import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service"; + +export class NoopSdkLoadService extends SdkLoadService { + async load() { + return; + } +} diff --git a/libs/common/src/platform/state/implementations/state-base.ts b/libs/common/src/platform/state/implementations/state-base.ts index 567de957e53..f285963d6e6 100644 --- a/libs/common/src/platform/state/implementations/state-base.ts +++ b/libs/common/src/platform/state/implementations/state-base.ts @@ -28,7 +28,7 @@ import { getStoredValue } from "./util"; // The parts of a KeyDefinition this class cares about to make it work type KeyDefinitionRequirements = { - deserializer: (jsonState: Jsonify) => T; + deserializer: (jsonState: Jsonify) => T | null; cleanupDelayMs: number; debug: Required; }; diff --git a/libs/common/src/platform/state/implementations/util.ts b/libs/common/src/platform/state/implementations/util.ts index f3d57fbafc4..0a9d76f6da5 100644 --- a/libs/common/src/platform/state/implementations/util.ts +++ b/libs/common/src/platform/state/implementations/util.ts @@ -5,12 +5,11 @@ import { AbstractStorageService } from "../../abstractions/storage.service"; export async function getStoredValue( key: string, storage: AbstractStorageService, - deserializer: (jsonValue: Jsonify) => T, + deserializer: (jsonValue: Jsonify) => T | null, ) { if (storage.valuesRequireDeserialization) { const jsonValue = await storage.get>(key); - const value = deserializer(jsonValue); - return value; + return deserializer(jsonValue); } else { const value = await storage.get(key); return value ?? null; diff --git a/libs/common/src/platform/state/key-definition.ts b/libs/common/src/platform/state/key-definition.ts index a270fc3e1a2..519e98ef52d 100644 --- a/libs/common/src/platform/state/key-definition.ts +++ b/libs/common/src/platform/state/key-definition.ts @@ -42,7 +42,7 @@ export type KeyDefinitionOptions = { * @param jsonValue The JSON object representation of your state. * @returns The fully typed version of your state. */ - readonly deserializer: (jsonValue: Jsonify) => T; + readonly deserializer: (jsonValue: Jsonify) => T | null; /** * The number of milliseconds to wait before cleaning up the state after the last subscriber has unsubscribed. * Defaults to 1000ms. diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index f40745142d0..03ea969c7bc 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -78,6 +78,7 @@ import { ApiKeyResponse } from "../auth/models/response/api-key.response"; import { AuthRequestResponse } from "../auth/models/response/auth-request.response"; import { DeviceVerificationResponse } from "../auth/models/response/device-verification.response"; import { IdentityCaptchaResponse } from "../auth/models/response/identity-captcha.response"; +import { IdentityDeviceVerificationResponse } from "../auth/models/response/identity-device-verification.response"; import { IdentityTokenResponse } from "../auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response"; import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response"; @@ -157,6 +158,13 @@ export class ApiService implements ApiServiceAbstraction { private deviceType: string; private isWebClient = false; private isDesktopClient = false; + private refreshTokenPromise: Promise | undefined; + + /** + * The message (responseJson.ErrorModel.Message) that comes back from the server when a new device verification is required. + */ + private static readonly NEW_DEVICE_VERIFICATION_REQUIRED_MESSAGE = + "new device verification required"; constructor( private tokenService: TokenService, @@ -197,7 +205,12 @@ export class ApiService implements ApiServiceAbstraction { | PasswordTokenRequest | SsoTokenRequest | WebAuthnLoginTokenRequest, - ): Promise { + ): Promise< + | IdentityTokenResponse + | IdentityTwoFactorResponse + | IdentityCaptchaResponse + | IdentityDeviceVerificationResponse + > { const headers = new Headers({ "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", Accept: "application/json", @@ -245,6 +258,11 @@ export class ApiService implements ApiServiceAbstraction { Object.keys(responseJson.HCaptcha_SiteKey).length ) { return new IdentityCaptchaResponse(responseJson); + } else if ( + response.status === 400 && + responseJson?.ErrorModel?.Message === ApiService.NEW_DEVICE_VERIFICATION_REQUIRED_MESSAGE + ) { + return new IdentityDeviceVerificationResponse(responseJson); } } @@ -1716,7 +1734,18 @@ export class ApiService implements ApiServiceAbstraction { ); } - protected async refreshToken(): Promise { + // Keep the running refreshTokenPromise to prevent parallel calls. + protected refreshToken(): Promise { + if (this.refreshTokenPromise === undefined) { + this.refreshTokenPromise = this.internalRefreshToken(); + void this.refreshTokenPromise.finally(() => { + this.refreshTokenPromise = undefined; + }); + } + return this.refreshTokenPromise; + } + + private async internalRefreshToken(): Promise { const refreshToken = await this.tokenService.getRefreshToken(); if (refreshToken != null && refreshToken !== "") { return this.refreshAccessToken(); diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 81b1016a53d..169de447f10 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -66,13 +66,14 @@ import { ForwarderOptionsMigrator } from "./migrations/65-migrate-forwarder-sett import { MoveFinalDesktopSettingsMigrator } from "./migrations/66-move-final-desktop-settings"; import { RemoveUnassignedItemsBannerDismissed } from "./migrations/67-remove-unassigned-items-banner-dismissed"; import { MoveLastSyncDate } from "./migrations/68-move-last-sync-date"; +import { MigrateIncorrectFolderKey } from "./migrations/69-migrate-incorrect-folder-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global"; import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 68; +export const CURRENT_VERSION = 69; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -142,7 +143,8 @@ export function createMigrationBuilder() { .with(ForwarderOptionsMigrator, 64, 65) .with(MoveFinalDesktopSettingsMigrator, 65, 66) .with(RemoveUnassignedItemsBannerDismissed, 66, 67) - .with(MoveLastSyncDate, 67, CURRENT_VERSION); + .with(MoveLastSyncDate, 67, 68) + .with(MigrateIncorrectFolderKey, 68, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.spec.ts b/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.spec.ts new file mode 100644 index 00000000000..e5dec943f78 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.spec.ts @@ -0,0 +1,98 @@ +import { runMigrator } from "../migration-helper.spec"; + +import { MigrateIncorrectFolderKey } from "./69-migrate-incorrect-folder-key"; + +function exampleJSON() { + return { + global_account_accounts: { + user1: null as any, + user2: null as any, + }, + user_user1_folder_folder: { + // Incorrect "folder" key + folderId1: { + id: "folderId1", + name: "folder-name-1", + revisionDate: "folder-revision-date-1", + }, + folderId2: { + id: "folderId2", + name: "folder-name-2", + revisionDate: "folder-revision-date-2", + }, + }, + user_user2_folder_folder: null as any, + }; +} + +describe("MigrateIncorrectFolderKey", () => { + const sut = new MigrateIncorrectFolderKey(68, 69); + it("migrates data", async () => { + const output = await runMigrator(sut, exampleJSON()); + + expect(output).toEqual({ + global_account_accounts: { + user1: null, + user2: null, + }, + user_user1_folder_folders: { + // Correct "folders" key + folderId1: { + id: "folderId1", + name: "folder-name-1", + revisionDate: "folder-revision-date-1", + }, + folderId2: { + id: "folderId2", + name: "folder-name-2", + revisionDate: "folder-revision-date-2", + }, + }, + }); + }); + + it("rolls back data", async () => { + const output = await runMigrator( + sut, + { + global_account_accounts: { + user1: null, + user2: null, + }, + user_user1_folder_folders: { + folderId1: { + id: "folderId1", + name: "folder-name-1", + revisionDate: "folder-revision-date-1", + }, + folderId2: { + id: "folderId2", + name: "folder-name-2", + revisionDate: "folder-revision-date-2", + }, + }, + }, + "rollback", + ); + + expect(output).toEqual({ + global_account_accounts: { + user1: null, + user2: null, + }, + user_user1_folder_folder: { + // Incorrect "folder" key + folderId1: { + id: "folderId1", + name: "folder-name-1", + revisionDate: "folder-revision-date-1", + }, + folderId2: { + id: "folderId2", + name: "folder-name-2", + revisionDate: "folder-revision-date-2", + }, + }, + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.ts b/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.ts new file mode 100644 index 00000000000..046c0cf0dfa --- /dev/null +++ b/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.ts @@ -0,0 +1,45 @@ +import { + KeyDefinitionLike, + MigrationHelper, +} from "@bitwarden/common/state-migrations/migration-helper"; +import { Migrator } from "@bitwarden/common/state-migrations/migrator"; + +const BAD_FOLDER_KEY: KeyDefinitionLike = { + key: "folder", // We inadvertently changed the key from "folders" to "folder" + stateDefinition: { + name: "folder", + }, +}; + +const GOOD_FOLDER_KEY: KeyDefinitionLike = { + key: "folders", // We should keep the key as "folders" + stateDefinition: { + name: "folder", + }, +}; + +export class MigrateIncorrectFolderKey extends Migrator<68, 69> { + async migrate(helper: MigrationHelper): Promise { + async function migrateUser(userId: string) { + const value = await helper.getFromUser(userId, BAD_FOLDER_KEY); + if (value != null) { + await helper.setToUser(userId, GOOD_FOLDER_KEY, value); + } + await helper.removeFromUser(userId, BAD_FOLDER_KEY); + } + const users = await helper.getKnownUserIds(); + await Promise.all(users.map((userId) => migrateUser(userId))); + } + + async rollback(helper: MigrationHelper): Promise { + async function rollbackUser(userId: string) { + const value = await helper.getFromUser(userId, GOOD_FOLDER_KEY); + if (value != null) { + await helper.setToUser(userId, BAD_FOLDER_KEY, value); + } + await helper.removeFromUser(userId, GOOD_FOLDER_KEY); + } + const users = await helper.getKnownUserIds(); + await Promise.all(users.map((userId) => rollbackUser(userId))); + } +} diff --git a/libs/common/src/tools/dependencies.ts b/libs/common/src/tools/dependencies.ts index c22e71cff67..befe1ca5406 100644 --- a/libs/common/src/tools/dependencies.ts +++ b/libs/common/src/tools/dependencies.ts @@ -1,7 +1,7 @@ import { Observable } from "rxjs"; -import { Policy } from "../admin-console/models/domain/policy"; -import { OrganizationId, UserId } from "../types/guid"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { OrganizationEncryptor } from "./cryptography/organization-encryptor.abstraction"; import { UserEncryptor } from "./cryptography/user-encryptor.abstraction"; @@ -152,7 +152,8 @@ export type SingleUserDependency = { }; /** A pattern for types that emit values exclusively when the dependency - * emits a message. + * emits a message. Set a type parameter when your method requires contextual + * information when the request is issued. * * Consumers of this dependency should emit when `on$` emits. If `on$` * completes, the consumer should also complete. If `on$` @@ -161,10 +162,10 @@ export type SingleUserDependency = { * @remarks This dependency is useful when you have a nondeterministic * or stateful algorithm that you would like to run when an event occurs. */ -export type OnDependency = { +export type OnDependency = { /** The stream that controls emissions */ - on$: Observable; + on$: Observable; }; /** A pattern for types that emit when a dependency is `true`. diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 88cd476606d..2e34f0ac601 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -93,7 +93,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider Promise; + ) => Promise; shareManyWithServer: ( ciphers: CipherView[], organizationId: string, diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index fe946fbb064..b1cdf72e08e 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -786,7 +786,7 @@ export class CipherService implements CipherServiceAbstraction { organizationId: string, collectionIds: string[], userId: UserId, - ): Promise { + ): Promise { const attachmentPromises: Promise[] = []; if (cipher.attachments != null) { cipher.attachments.forEach((attachment) => { @@ -806,6 +806,7 @@ export class CipherService implements CipherServiceAbstraction { const response = await this.apiService.putShareCipher(cipher.id, request); const data = new CipherData(response, collectionIds); await this.upsert(data); + return new Cipher(data, cipher.localData); } async shareManyWithServer( diff --git a/libs/common/src/vault/services/key-state/folder.state.ts b/libs/common/src/vault/services/key-state/folder.state.ts index 99ad8e5ae35..b3e61f5bf31 100644 --- a/libs/common/src/vault/services/key-state/folder.state.ts +++ b/libs/common/src/vault/services/key-state/folder.state.ts @@ -6,7 +6,7 @@ import { FolderView } from "../../models/view/folder.view"; export const FOLDER_ENCRYPTED_FOLDERS = UserKeyDefinition.record( FOLDER_DISK, - "folder", + "folders", { deserializer: (obj: Jsonify) => FolderData.fromJSON(obj), clearOn: ["logout"], diff --git a/libs/key-management/package.json b/libs/key-management/package.json index 083386cbc81..1063e16580e 100644 --- a/libs/key-management/package.json +++ b/libs/key-management/package.json @@ -15,7 +15,8 @@ "scripts": { "clean": "rimraf dist", "build": "npm run clean && tsc", - "build:watch": "npm run clean && tsc -watch" + "build:watch": "npm run clean && tsc -watch", + "test": "jest" }, "dependencies": { "@bitwarden/angular": "file:../angular", diff --git a/libs/key-management/src/abstractions/kdf-config.service.ts b/libs/key-management/src/abstractions/kdf-config.service.ts index d5f613828d4..9cc39561aa8 100644 --- a/libs/key-management/src/abstractions/kdf-config.service.ts +++ b/libs/key-management/src/abstractions/kdf-config.service.ts @@ -7,5 +7,5 @@ import { KdfConfig } from "../models/kdf-config"; export abstract class KdfConfigService { abstract setKdfConfig(userId: UserId, KdfConfig: KdfConfig): Promise; abstract getKdfConfig(): Promise; - abstract getKdfConfig$(userId: UserId): Observable; + abstract getKdfConfig$(userId: UserId): Observable; } diff --git a/libs/key-management/src/kdf-config.service.spec.ts b/libs/key-management/src/kdf-config.service.spec.ts index 76a13fe10fc..986d7abac40 100644 --- a/libs/key-management/src/kdf-config.service.spec.ts +++ b/libs/key-management/src/kdf-config.service.spec.ts @@ -1,18 +1,15 @@ +import { firstValueFrom } from "rxjs"; + +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { FakeAccountService, FakeStateProvider, mockAccountServiceWith, } from "@bitwarden/common/spec"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { UserId } from "@bitwarden/common/src/types/guid"; - -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { Utils } from "../../common/src/platform/misc/utils"; +import { UserId } from "@bitwarden/common/types/guid"; -import { DefaultKdfConfigService } from "./kdf-config.service"; -import { Argon2KdfConfig, PBKDF2KdfConfig } from "./models/kdf-config"; +import { DefaultKdfConfigService, KDF_CONFIG } from "./kdf-config.service"; +import { Argon2KdfConfig, KdfConfig, PBKDF2KdfConfig } from "./models/kdf-config"; describe("KdfConfigService", () => { let sutKdfConfigService: DefaultKdfConfigService; @@ -29,113 +26,90 @@ describe("KdfConfigService", () => { sutKdfConfigService = new DefaultKdfConfigService(fakeStateProvider); }); - it("setKdfConfig(): should set the KDF config", async () => { - const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(600_000); + it("setKdfConfig(): should set the PBKDF2KdfConfig config", async () => { + const kdfConfig: KdfConfig = new PBKDF2KdfConfig(500_000); await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig); - await expect(sutKdfConfigService.getKdfConfig()).resolves.toEqual(kdfConfig); + expect(fakeStateProvider.mock.setUserState).toHaveBeenCalledWith( + KDF_CONFIG, + kdfConfig, + mockUserId, + ); }); - it("setKdfConfig(): should get the KDF config", async () => { - const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4); + it("setKdfConfig(): should set the Argon2KdfConfig config", async () => { + const kdfConfig: KdfConfig = new Argon2KdfConfig(2, 63, 3); await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig); - await expect(sutKdfConfigService.getKdfConfig()).resolves.toEqual(kdfConfig); + expect(fakeStateProvider.mock.setUserState).toHaveBeenCalledWith( + KDF_CONFIG, + kdfConfig, + mockUserId, + ); }); it("setKdfConfig(): should throw error KDF cannot be null", async () => { - const kdfConfig: Argon2KdfConfig = null; try { - await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig); + await sutKdfConfigService.setKdfConfig(mockUserId, null as unknown as KdfConfig); } catch (e) { expect(e).toEqual(new Error("kdfConfig cannot be null")); } }); it("setKdfConfig(): should throw error userId cannot be null", async () => { - const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4); + const kdfConfig: KdfConfig = new Argon2KdfConfig(3, 64, 4); try { - await sutKdfConfigService.setKdfConfig(null, kdfConfig); + await sutKdfConfigService.setKdfConfig(null as unknown as UserId, kdfConfig); } catch (e) { expect(e).toEqual(new Error("userId cannot be null")); } }); - it("getKdfConfig(): should throw error KdfConfig for active user account state is null", async () => { + it("getKdfConfig(): should get KdfConfig of active user", async () => { + const kdfConfig: KdfConfig = new PBKDF2KdfConfig(500_000); + await fakeStateProvider.setUserState(KDF_CONFIG, kdfConfig, mockUserId); + await expect(sutKdfConfigService.getKdfConfig()).resolves.toEqual(kdfConfig); + }); + + it("getKdfConfig(): should throw error KdfConfig can only be retrieved when there is active user", async () => { + fakeAccountService.activeAccountSubject.next(null); try { await sutKdfConfigService.getKdfConfig(); } catch (e) { - expect(e).toEqual(new Error("KdfConfig for active user account state is null")); + expect(e).toEqual(new Error("KdfConfig can only be retrieved when there is active user")); } }); - it("validateKdfConfigForSetting(): should validate the PBKDF2 KDF config", () => { - const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(600_000); - expect(() => kdfConfig.validateKdfConfigForSetting()).not.toThrow(); - }); - - it("validateKdfConfigForSetting(): should validate the Argon2id KDF config", () => { - const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4); - expect(() => kdfConfig.validateKdfConfigForSetting()).not.toThrow(); - }); - - it("validateKdfConfigForSetting(): should throw an error for invalid PBKDF2 iterations", () => { - const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(100000); - expect(() => kdfConfig.validateKdfConfigForSetting()).toThrow( - `PBKDF2 iterations must be between ${PBKDF2KdfConfig.ITERATIONS.min} and ${PBKDF2KdfConfig.ITERATIONS.max}`, - ); - }); - - it("validateKdfConfigForSetting(): should throw an error for invalid Argon2 iterations", () => { - const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(11, 64, 4); - expect(() => kdfConfig.validateKdfConfigForSetting()).toThrow( - `Argon2 iterations must be between ${Argon2KdfConfig.ITERATIONS.min} and ${Argon2KdfConfig.ITERATIONS.max}`, - ); - }); - - it("validateKdfConfigForSetting(): should throw an error for invalid Argon2 parallelism", () => { - const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 17); - expect(() => kdfConfig.validateKdfConfigForSetting()).toThrow( - `Argon2 parallelism must be between ${Argon2KdfConfig.PARALLELISM.min} and ${Argon2KdfConfig.PARALLELISM.max}`, - ); - }); - - it("validateKdfConfigForPrelogin(): should validate the PBKDF2 KDF config", () => { - const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(600_000); - expect(() => kdfConfig.validateKdfConfigForPrelogin()).not.toThrow(); - }); - - it("validateKdfConfigForPrelogin(): should validate the Argon2id KDF config", () => { - const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4); - expect(() => kdfConfig.validateKdfConfigForPrelogin()).not.toThrow(); + it("getKdfConfig(): should throw error KdfConfig for active user account state is null", async () => { + try { + await sutKdfConfigService.getKdfConfig(); + } catch (e) { + expect(e).toEqual(new Error("KdfConfig for active user account state is null")); + } }); - it("validateKdfConfigForPrelogin(): should throw an error for too low PBKDF2 iterations", () => { - const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig( - PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN - 1, - ); - expect(() => kdfConfig.validateKdfConfigForPrelogin()).toThrow( - `PBKDF2 iterations must be at least ${PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN}, but was ${kdfConfig.iterations}; possible pre-login downgrade attack detected.`, + it("getKdfConfig$(UserId): should get KdfConfig of provided user", async () => { + await expect(firstValueFrom(sutKdfConfigService.getKdfConfig$(mockUserId))).resolves.toBeNull(); + const kdfConfig: KdfConfig = new PBKDF2KdfConfig(500_000); + await fakeStateProvider.setUserState(KDF_CONFIG, kdfConfig, mockUserId); + await expect(firstValueFrom(sutKdfConfigService.getKdfConfig$(mockUserId))).resolves.toEqual( + kdfConfig, ); }); - it("validateKdfConfigForPrelogin(): should throw an error for too low Argon2 iterations", () => { - const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig( - Argon2KdfConfig.PRELOGIN_ITERATIONS_MIN - 1, - 64, - 4, - ); - expect(() => kdfConfig.validateKdfConfigForPrelogin()).toThrow( - `Argon2 iterations must be at least ${Argon2KdfConfig.PRELOGIN_ITERATIONS_MIN}, but was ${kdfConfig.iterations}; possible pre-login downgrade attack detected.`, + it("getKdfConfig$(UserId): should get KdfConfig of provided user after changed", async () => { + await expect(firstValueFrom(sutKdfConfigService.getKdfConfig$(mockUserId))).resolves.toBeNull(); + await fakeStateProvider.setUserState(KDF_CONFIG, new PBKDF2KdfConfig(500_000), mockUserId); + const kdfConfigChanged: KdfConfig = new PBKDF2KdfConfig(500_001); + await fakeStateProvider.setUserState(KDF_CONFIG, kdfConfigChanged, mockUserId); + await expect(firstValueFrom(sutKdfConfigService.getKdfConfig$(mockUserId))).resolves.toEqual( + kdfConfigChanged, ); }); - it("validateKdfConfigForPrelogin(): should throw an error for too low Argon2 memory", () => { - const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig( - 3, - Argon2KdfConfig.PRELOGIN_MEMORY_MIN - 1, - 4, - ); - expect(() => kdfConfig.validateKdfConfigForPrelogin()).toThrow( - `Argon2 memory must be at least ${Argon2KdfConfig.PRELOGIN_MEMORY_MIN} MiB, but was ${kdfConfig.memory} MiB; possible pre-login downgrade attack detected.`, - ); + it("getKdfConfig$(UserId): should throw error userId cannot be null", async () => { + try { + sutKdfConfigService.getKdfConfig$(null as unknown as UserId); + } catch (e) { + expect(e).toEqual(new Error("userId cannot be null")); + } }); }); diff --git a/libs/key-management/src/kdf-config.service.ts b/libs/key-management/src/kdf-config.service.ts index f0c964c5d2e..efc5310e5a8 100644 --- a/libs/key-management/src/kdf-config.service.ts +++ b/libs/key-management/src/kdf-config.service.ts @@ -1,21 +1,19 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { firstValueFrom, Observable } from "rxjs"; +import { Jsonify } from "type-fest/source/jsonify"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { UserId } from "@bitwarden/common/src/types/guid"; - -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KDF_CONFIG_DISK, StateProvider, UserKeyDefinition } from "../../common/src/platform/state"; +import { + KDF_CONFIG_DISK, + StateProvider, + UserKeyDefinition, +} from "@bitwarden/common/platform/state"; +import { UserId } from "@bitwarden/common/types/guid"; import { KdfConfigService } from "./abstractions/kdf-config.service"; import { KdfType } from "./enums/kdf-type.enum"; import { Argon2KdfConfig, KdfConfig, PBKDF2KdfConfig } from "./models/kdf-config"; export const KDF_CONFIG = new UserKeyDefinition(KDF_CONFIG_DISK, "kdfConfig", { - deserializer: (kdfConfig: KdfConfig) => { + deserializer: (kdfConfig: Jsonify) => { if (kdfConfig == null) { return null; } @@ -28,11 +26,12 @@ export const KDF_CONFIG = new UserKeyDefinition(KDF_CONFIG_DISK, "kdf export class DefaultKdfConfigService implements KdfConfigService { constructor(private stateProvider: StateProvider) {} + async setKdfConfig(userId: UserId, kdfConfig: KdfConfig) { - if (!userId) { + if (userId == null) { throw new Error("userId cannot be null"); } - if (kdfConfig === null) { + if (kdfConfig == null) { throw new Error("kdfConfig cannot be null"); } await this.stateProvider.setUserState(KDF_CONFIG, kdfConfig, userId); @@ -40,14 +39,20 @@ export class DefaultKdfConfigService implements KdfConfigService { async getKdfConfig(): Promise { const userId = await firstValueFrom(this.stateProvider.activeUserId$); + if (userId == null) { + throw new Error("KdfConfig can only be retrieved when there is active user"); + } const state = await firstValueFrom(this.stateProvider.getUser(userId, KDF_CONFIG).state$); - if (state === null) { + if (state == null) { throw new Error("KdfConfig for active user account state is null"); } return state; } - getKdfConfig$(userId: UserId): Observable { + getKdfConfig$(userId: UserId): Observable { + if (userId == null) { + throw new Error("userId cannot be null"); + } return this.stateProvider.getUser(userId, KDF_CONFIG).state$; } } diff --git a/libs/key-management/src/models/kdf-config.spec.ts b/libs/key-management/src/models/kdf-config.spec.ts new file mode 100644 index 00000000000..347f574fc00 --- /dev/null +++ b/libs/key-management/src/models/kdf-config.spec.ts @@ -0,0 +1,75 @@ +import { Argon2KdfConfig, PBKDF2KdfConfig } from "./kdf-config"; + +describe("KdfConfig", () => { + it("validateKdfConfigForSetting(): should validate the PBKDF2 KDF config", () => { + const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(600_000); + expect(() => kdfConfig.validateKdfConfigForSetting()).not.toThrow(); + }); + + it("validateKdfConfigForSetting(): should validate the Argon2id KDF config", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4); + expect(() => kdfConfig.validateKdfConfigForSetting()).not.toThrow(); + }); + + it("validateKdfConfigForSetting(): should throw an error for invalid PBKDF2 iterations", () => { + const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(100000); + expect(() => kdfConfig.validateKdfConfigForSetting()).toThrow( + `PBKDF2 iterations must be between ${PBKDF2KdfConfig.ITERATIONS.min} and ${PBKDF2KdfConfig.ITERATIONS.max}`, + ); + }); + + it("validateKdfConfigForSetting(): should throw an error for invalid Argon2 iterations", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(11, 64, 4); + expect(() => kdfConfig.validateKdfConfigForSetting()).toThrow( + `Argon2 iterations must be between ${Argon2KdfConfig.ITERATIONS.min} and ${Argon2KdfConfig.ITERATIONS.max}`, + ); + }); + + it("validateKdfConfigForSetting(): should throw an error for invalid Argon2 parallelism", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 17); + expect(() => kdfConfig.validateKdfConfigForSetting()).toThrow( + `Argon2 parallelism must be between ${Argon2KdfConfig.PARALLELISM.min} and ${Argon2KdfConfig.PARALLELISM.max}`, + ); + }); + + it("validateKdfConfigForPrelogin(): should validate the PBKDF2 KDF config", () => { + const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(600_000); + expect(() => kdfConfig.validateKdfConfigForPrelogin()).not.toThrow(); + }); + + it("validateKdfConfigForPrelogin(): should validate the Argon2id KDF config", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4); + expect(() => kdfConfig.validateKdfConfigForPrelogin()).not.toThrow(); + }); + + it("validateKdfConfigForPrelogin(): should throw an error for too low PBKDF2 iterations", () => { + const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig( + PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN - 1, + ); + expect(() => kdfConfig.validateKdfConfigForPrelogin()).toThrow( + `PBKDF2 iterations must be at least ${PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN}, but was ${kdfConfig.iterations}; possible pre-login downgrade attack detected.`, + ); + }); + + it("validateKdfConfigForPrelogin(): should throw an error for too low Argon2 iterations", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig( + Argon2KdfConfig.PRELOGIN_ITERATIONS_MIN - 1, + 64, + 4, + ); + expect(() => kdfConfig.validateKdfConfigForPrelogin()).toThrow( + `Argon2 iterations must be at least ${Argon2KdfConfig.PRELOGIN_ITERATIONS_MIN}, but was ${kdfConfig.iterations}; possible pre-login downgrade attack detected.`, + ); + }); + + it("validateKdfConfigForPrelogin(): should throw an error for too low Argon2 memory", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig( + 3, + Argon2KdfConfig.PRELOGIN_MEMORY_MIN - 1, + 4, + ); + expect(() => kdfConfig.validateKdfConfigForPrelogin()).toThrow( + `Argon2 memory must be at least ${Argon2KdfConfig.PRELOGIN_MEMORY_MIN} MiB, but was ${kdfConfig.memory} MiB; possible pre-login downgrade attack detected.`, + ); + }); +}); diff --git a/libs/key-management/src/models/kdf-config.ts b/libs/key-management/src/models/kdf-config.ts index fe9c0de255c..689da77c163 100644 --- a/libs/key-management/src/models/kdf-config.ts +++ b/libs/key-management/src/models/kdf-config.ts @@ -1,8 +1,7 @@ import { Jsonify } from "type-fest"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { RangeWithDefault } from "../../../common/src/platform/misc/range-with-default"; +import { RangeWithDefault } from "@bitwarden/common/platform/misc/range-with-default"; + import { KdfType } from "../enums/kdf-type.enum"; /** diff --git a/libs/tools/generator/components/src/credential-generator-history.component.ts b/libs/tools/generator/components/src/credential-generator-history.component.ts index 69ed0b0336d..7e476564de6 100644 --- a/libs/tools/generator/components/src/credential-generator-history.component.ts +++ b/libs/tools/generator/components/src/credential-generator-history.component.ts @@ -72,6 +72,6 @@ export class CredentialGeneratorHistoryComponent { protected getGeneratedValueText(credential: GeneratedCredential) { const info = this.generatorService.algorithm(credential.category); - return info.generatedValue; + return info.credentialType; } } diff --git a/libs/tools/generator/components/src/credential-generator.component.html b/libs/tools/generator/components/src/credential-generator.component.html index 6f27c7020db..624c5ab5860 100644 --- a/libs/tools/generator/components/src/credential-generator.component.html +++ b/libs/tools/generator/components/src/credential-generator.component.html @@ -20,7 +20,7 @@ type="button" bitIconButton="bwi-generate" buttonType="main" - (click)="generate('user request')" + (click)="generate(USER_REQUEST)" [appA11yTitle]="credentialTypeGenerateLabel$ | async" [disabled]="!(algorithm$ | async)" > diff --git a/libs/tools/generator/components/src/credential-generator.component.ts b/libs/tools/generator/components/src/credential-generator.component.ts index a2b204eaca4..04c7e5e8d87 100644 --- a/libs/tools/generator/components/src/credential-generator.component.ts +++ b/libs/tools/generator/components/src/credential-generator.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { LiveAnnouncer } from "@angular/cdk/a11y"; import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { @@ -28,6 +29,7 @@ import { CredentialAlgorithm, CredentialCategory, CredentialGeneratorService, + GenerateRequest, GeneratedCredential, Generators, getForwarderConfiguration, @@ -60,6 +62,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { private accountService: AccountService, private zone: NgZone, private formBuilder: FormBuilder, + private ariaLive: LiveAnnouncer, ) {} /** Binds the component to a specific user's settings. When this input is not provided, @@ -185,10 +188,10 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { // continue with origin stream return generator; }), - withLatestFrom(this.userId$), + withLatestFrom(this.userId$, this.algorithm$), takeUntil(this.destroyed), ) - .subscribe(([generated, userId]) => { + .subscribe(([generated, userId, algorithm]) => { this.generatorHistoryService .track(userId, generated.credential, generated.category, generated.generationDate) .catch((e: unknown) => { @@ -198,8 +201,12 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { // update subjects within the angular zone so that the // template bindings refresh immediately this.zone.run(() => { + if (generated.source === this.USER_REQUEST) { + this.announce(algorithm.onGeneratedMessage); + } + + this.generatedCredential$.next(generated); this.onGenerated.next(generated); - this.value$.next(generated.credential); }); }); @@ -383,7 +390,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { this.algorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => { this.zone.run(() => { if (!a || a.onlyOnRequest) { - this.value$.next("-"); + this.generatedCredential$.next(null); } else { this.generate("autogenerate").catch((e: unknown) => this.logService.error(e)); } @@ -391,6 +398,10 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { }); } + private announce(message: string) { + this.ariaLive.announce(message).catch((e) => this.logService.error(e)); + } + private typeToGenerator$(type: CredentialAlgorithm) { const dependencies = { on$: this.generate$, @@ -473,7 +484,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { */ protected credentialTypeLabel$ = this.algorithm$.pipe( filter((algorithm) => !!algorithm), - map(({ generatedValue }) => generatedValue), + map(({ credentialType }) => credentialType), ); /** Emits hint key for the currently selected credential type */ @@ -482,21 +493,28 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { /** tracks the currently selected credential category */ protected category$ = new ReplaySubject(1); + private readonly generatedCredential$ = new BehaviorSubject(null); + /** Emits the last generated value. */ - protected readonly value$ = new BehaviorSubject(""); + protected readonly value$ = this.generatedCredential$.pipe( + map((generated) => generated?.credential ?? "-"), + ); /** Emits when the userId changes */ protected readonly userId$ = new BehaviorSubject(null); + /** Identifies generator requests that were requested by the user */ + protected readonly USER_REQUEST = "user request"; + /** Emits when a new credential is requested */ - private readonly generate$ = new Subject(); + private readonly generate$ = new Subject(); /** Request a new value from the generator * @param requestor a label used to trace generation request * origin in the debugger. */ protected async generate(requestor: string) { - this.generate$.next(requestor); + this.generate$.next({ source: requestor }); } private toOptions(algorithms: AlgorithmInfo[]) { @@ -515,7 +533,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { // finalize subjects this.generate$.complete(); - this.value$.complete(); + this.generatedCredential$.complete(); // finalize component bindings this.onGenerated.complete(); diff --git a/libs/tools/generator/components/src/password-generator.component.html b/libs/tools/generator/components/src/password-generator.component.html index a6aa5ebdd02..c7fa93dc535 100644 --- a/libs/tools/generator/components/src/password-generator.component.html +++ b/libs/tools/generator/components/src/password-generator.component.html @@ -18,7 +18,7 @@ type="button" bitIconButton="bwi-generate" buttonType="main" - (click)="generate('user request')" + (click)="generate(USER_REQUEST)" [appA11yTitle]="credentialTypeGenerateLabel$ | async" [disabled]="!(algorithm$ | async)" > diff --git a/libs/tools/generator/components/src/password-generator.component.ts b/libs/tools/generator/components/src/password-generator.component.ts index 85363412ffa..b59b162e687 100644 --- a/libs/tools/generator/components/src/password-generator.component.ts +++ b/libs/tools/generator/components/src/password-generator.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { LiveAnnouncer } from "@angular/cdk/a11y"; import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core"; import { @@ -16,7 +17,6 @@ import { } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { UserId } from "@bitwarden/common/types/guid"; import { ToastService, Option } from "@bitwarden/components"; @@ -28,6 +28,7 @@ import { isPasswordAlgorithm, AlgorithmInfo, isSameAlgorithm, + GenerateRequest, } from "@bitwarden/generator-core"; import { GeneratorHistoryService } from "@bitwarden/generator-history"; @@ -42,9 +43,9 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { private generatorHistoryService: GeneratorHistoryService, private toastService: ToastService, private logService: LogService, - private i18nService: I18nService, private accountService: AccountService, private zone: NgZone, + private ariaLive: LiveAnnouncer, ) {} /** Binds the component to a specific user's settings. @@ -67,14 +68,17 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { protected readonly userId$ = new BehaviorSubject(null); /** Emits when a new credential is requested */ - private readonly generate$ = new Subject(); + private readonly generate$ = new Subject(); + + /** Identifies generator requests that were requested by the user */ + protected readonly USER_REQUEST = "user request"; /** Request a new value from the generator * @param requestor a label used to trace generation request * origin in the debugger. */ protected async generate(requestor: string) { - this.generate$.next(requestor); + this.generate$.next({ source: requestor }); } /** Tracks changes to the selected credential type @@ -137,10 +141,10 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { // continue with origin stream return generator; }), - withLatestFrom(this.userId$), + withLatestFrom(this.userId$, this.algorithm$), takeUntil(this.destroyed), ) - .subscribe(([generated, userId]) => { + .subscribe(([generated, userId, algorithm]) => { this.generatorHistoryService .track(userId, generated.credential, generated.category, generated.generationDate) .catch((e: unknown) => { @@ -150,6 +154,10 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { // update subjects within the angular zone so that the // template bindings refresh immediately this.zone.run(() => { + if (generated.source === this.USER_REQUEST) { + this.announce(algorithm.onGeneratedMessage); + } + this.onGenerated.next(generated); this.value$.next(generated.credential); }); @@ -205,6 +213,10 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { }); } + private announce(message: string) { + this.ariaLive.announce(message).catch((e) => this.logService.error(e)); + } + private typeToGenerator$(type: CredentialAlgorithm) { const dependencies = { on$: this.generate$, @@ -249,7 +261,7 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { */ protected credentialTypeLabel$ = this.algorithm$.pipe( filter((algorithm) => !!algorithm), - map(({ generatedValue }) => generatedValue), + map(({ credentialType }) => credentialType), ); private toOptions(algorithms: AlgorithmInfo[]) { diff --git a/libs/tools/generator/components/src/username-generator.component.html b/libs/tools/generator/components/src/username-generator.component.html index a5effcc0f99..20cb0ec31bd 100644 --- a/libs/tools/generator/components/src/username-generator.component.html +++ b/libs/tools/generator/components/src/username-generator.component.html @@ -7,7 +7,7 @@ type="button" bitIconButton="bwi-generate" buttonType="main" - (click)="generate('user request')" + (click)="generate(USER_REQUEST)" [appA11yTitle]="credentialTypeGenerateLabel$ | async" [disabled]="!(algorithm$ | async)" > diff --git a/libs/tools/generator/components/src/username-generator.component.ts b/libs/tools/generator/components/src/username-generator.component.ts index 63c1adc602b..e521fea8e28 100644 --- a/libs/tools/generator/components/src/username-generator.component.ts +++ b/libs/tools/generator/components/src/username-generator.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { LiveAnnouncer } from "@angular/cdk/a11y"; import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder } from "@angular/forms"; @@ -28,6 +29,7 @@ import { AlgorithmInfo, CredentialAlgorithm, CredentialGeneratorService, + GenerateRequest, GeneratedCredential, Generators, getForwarderConfiguration, @@ -66,6 +68,7 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { private accountService: AccountService, private zone: NgZone, private formBuilder: FormBuilder, + private ariaLive: LiveAnnouncer, ) {} /** Binds the component to a specific user's settings. When this input is not provided, @@ -160,10 +163,10 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { // continue with origin stream return generator; }), - withLatestFrom(this.userId$), + withLatestFrom(this.userId$, this.algorithm$), takeUntil(this.destroyed), ) - .subscribe(([generated, userId]) => { + .subscribe(([generated, userId, algorithm]) => { this.generatorHistoryService .track(userId, generated.credential, generated.category, generated.generationDate) .catch((e: unknown) => { @@ -173,6 +176,10 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { // update subjects within the angular zone so that the // template bindings refresh immediately this.zone.run(() => { + if (generated.source === this.USER_REQUEST) { + this.announce(algorithm.onGeneratedMessage); + } + this.onGenerated.next(generated); this.value$.next(generated.credential); }); @@ -360,6 +367,10 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { throw new Error(`Invalid generator type: "${type}"`); } + private announce(message: string) { + this.ariaLive.announce(message).catch((e) => this.logService.error(e)); + } + /** Lists the credential types supported by the component. */ protected typeOptions$ = new BehaviorSubject[]>([]); @@ -375,6 +386,18 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { /** tracks the currently selected credential type */ protected algorithm$ = new ReplaySubject(1); + /** Emits hint key for the currently selected credential type */ + protected credentialTypeHint$ = new ReplaySubject(1); + + /** Emits the last generated value. */ + protected readonly value$ = new BehaviorSubject(""); + + /** Emits when the userId changes */ + protected readonly userId$ = new BehaviorSubject(null); + + /** Emits when a new credential is requested */ + private readonly generate$ = new Subject(); + protected showAlgorithm$ = this.algorithm$.pipe( combineLatestWith(this.showForwarder$), map(([algorithm, showForwarder]) => (showForwarder ? null : algorithm)), @@ -401,27 +424,18 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { */ protected credentialTypeLabel$ = this.algorithm$.pipe( filter((algorithm) => !!algorithm), - map(({ generatedValue }) => generatedValue), + map(({ credentialType }) => credentialType), ); - /** Emits hint key for the currently selected credential type */ - protected credentialTypeHint$ = new ReplaySubject(1); - - /** Emits the last generated value. */ - protected readonly value$ = new BehaviorSubject(""); - - /** Emits when the userId changes */ - protected readonly userId$ = new BehaviorSubject(null); - - /** Emits when a new credential is requested */ - private readonly generate$ = new Subject(); + /** Identifies generator requests that were requested by the user */ + protected readonly USER_REQUEST = "user request"; /** Request a new value from the generator * @param requestor a label used to trace generation request * origin in the debugger. */ protected async generate(requestor: string) { - this.generate$.next(requestor); + this.generate$.next({ source: requestor }); } private toOptions(algorithms: AlgorithmInfo[]) { diff --git a/libs/tools/generator/core/src/data/generators.ts b/libs/tools/generator/core/src/data/generators.ts index 12209b402e0..da87c60f1f4 100644 --- a/libs/tools/generator/core/src/data/generators.ts +++ b/libs/tools/generator/core/src/data/generators.ts @@ -56,7 +56,8 @@ const PASSPHRASE: CredentialGeneratorConfiguration< category: "password", nameKey: "passphrase", generateKey: "generatePassphrase", - generatedValueKey: "passphrase", + onGeneratedMessageKey: "passphraseGenerated", + credentialTypeKey: "passphrase", copyKey: "copyPassphrase", useGeneratedValueKey: "useThisPassword", onlyOnRequest: false, @@ -118,7 +119,8 @@ const PASSWORD: CredentialGeneratorConfiguration< category: "password", nameKey: "password", generateKey: "generatePassword", - generatedValueKey: "password", + onGeneratedMessageKey: "passwordGenerated", + credentialTypeKey: "password", copyKey: "copyPassword", useGeneratedValueKey: "useThisPassword", onlyOnRequest: false, @@ -195,7 +197,8 @@ const USERNAME: CredentialGeneratorConfiguration; generate( - request: GenerationRequest, + request: GenerateRequest, settings: SubaddressGenerationOptions, ): Promise; async generate( - _request: GenerationRequest, + request: GenerateRequest, settings: CatchallGenerationOptions | SubaddressGenerationOptions, ) { if (isCatchallGenerationOptions(settings)) { const email = await this.randomAsciiCatchall(settings.catchallDomain); - return new GeneratedCredential(email, "catchall", Date.now()); + return new GeneratedCredential( + email, + "catchall", + Date.now(), + request.source, + request.website, + ); } else if (isSubaddressGenerationOptions(settings)) { const email = await this.randomAsciiSubaddress(settings.subaddressEmail); - return new GeneratedCredential(email, "subaddress", Date.now()); + return new GeneratedCredential( + email, + "subaddress", + Date.now(), + request.source, + request.website, + ); } throw new Error("Invalid settings received by generator."); diff --git a/libs/tools/generator/core/src/engine/password-randomizer.ts b/libs/tools/generator/core/src/engine/password-randomizer.ts index c1e9aed7b8b..a9612d2fb45 100644 --- a/libs/tools/generator/core/src/engine/password-randomizer.ts +++ b/libs/tools/generator/core/src/engine/password-randomizer.ts @@ -1,10 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist"; -import { GenerationRequest } from "@bitwarden/common/tools/types"; import { CredentialGenerator, + GenerateRequest, GeneratedCredential, PassphraseGenerationOptions, PasswordGenerationOptions, @@ -69,27 +69,39 @@ export class PasswordRandomizer } generate( - request: GenerationRequest, + request: GenerateRequest, settings: PasswordGenerationOptions, ): Promise; generate( - request: GenerationRequest, + request: GenerateRequest, settings: PassphraseGenerationOptions, ): Promise; async generate( - _request: GenerationRequest, + request: GenerateRequest, settings: PasswordGenerationOptions | PassphraseGenerationOptions, ) { if (isPasswordGenerationOptions(settings)) { - const request = optionsToRandomAsciiRequest(settings); - const password = await this.randomAscii(request); - - return new GeneratedCredential(password, "password", Date.now()); + const req = optionsToRandomAsciiRequest(settings); + const password = await this.randomAscii(req); + + return new GeneratedCredential( + password, + "password", + Date.now(), + request.source, + request.website, + ); } else if (isPassphraseGenerationOptions(settings)) { - const request = optionsToEffWordListRequest(settings); - const passphrase = await this.randomEffLongWords(request); - - return new GeneratedCredential(passphrase, "passphrase", Date.now()); + const req = optionsToEffWordListRequest(settings); + const passphrase = await this.randomEffLongWords(req); + + return new GeneratedCredential( + passphrase, + "passphrase", + Date.now(), + request.source, + request.website, + ); } throw new Error("Invalid settings received by generator."); diff --git a/libs/tools/generator/core/src/engine/username-randomizer.ts b/libs/tools/generator/core/src/engine/username-randomizer.ts index df608553839..d13066c7e55 100644 --- a/libs/tools/generator/core/src/engine/username-randomizer.ts +++ b/libs/tools/generator/core/src/engine/username-randomizer.ts @@ -1,7 +1,11 @@ import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist"; -import { GenerationRequest } from "@bitwarden/common/tools/types"; -import { CredentialGenerator, EffUsernameGenerationOptions, GeneratedCredential } from "../types"; +import { + CredentialGenerator, + EffUsernameGenerationOptions, + GenerateRequest, + GeneratedCredential, +} from "../types"; import { Randomizer } from "./abstractions"; import { WordsRequest } from "./types"; @@ -51,14 +55,20 @@ export class UsernameRandomizer implements CredentialGenerator { return { generate: (request, settings) => { - const credential = request.website ? `${request.website}|${settings.foo}` : settings.foo; - const result = new GeneratedCredential(credential, SomeAlgorithm, SomeTime); + const result = new GeneratedCredential( + settings.foo, + SomeAlgorithm, + SomeTime, + request.source, + request.website, + ); return Promise.resolve(result); }, }; @@ -191,30 +201,8 @@ describe("CredentialGeneratorService", () => { }); describe("generate$", () => { - it("emits a generation for the active user when subscribed", async () => { - const settings = { foo: "value" }; - await stateProvider.setUserState(SettingsKey, settings, SomeUser); - const generator = new CredentialGeneratorService( - randomizer, - stateProvider, - policyService, - apiService, - i18nService, - encryptorProvider, - accountService, - ); - const generated = new ObservableTracker(generator.generate$(SomeConfiguration)); - - const result = await generated.expectEmission(); - - expect(result).toEqual(new GeneratedCredential("value", SomeAlgorithm, SomeTime)); - }); - - it("follows the active user", async () => { - const someSettings = { foo: "some value" }; - const anotherSettings = { foo: "another value" }; - await stateProvider.setUserState(SettingsKey, someSettings, SomeUser); - await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser); + it("completes when `on$` completes", async () => { + await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); const generator = new CredentialGeneratorService( randomizer, stateProvider, @@ -224,22 +212,24 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); - const generated = new ObservableTracker(generator.generate$(SomeConfiguration)); + const on$ = new Subject(); + let complete = false; - await accountService.switchAccount(AnotherUser); - await generated.pauseUntilReceived(2); - generated.unsubscribe(); + // confirm no emission during subscription + generator.generate$(SomeConfiguration, { on$ }).subscribe({ + complete: () => { + complete = true; + }, + }); + on$.complete(); + await awaitAsync(); - expect(generated.emissions).toEqual([ - new GeneratedCredential("some value", SomeAlgorithm, SomeTime), - new GeneratedCredential("another value", SomeAlgorithm, SomeTime), - ]); + expect(complete).toBeTruthy(); }); - it("emits a generation when the settings change", async () => { - const someSettings = { foo: "some value" }; - const anotherSettings = { foo: "another value" }; - await stateProvider.setUserState(SettingsKey, someSettings, SomeUser); + it("includes request.source in the generated credential", async () => { + const settings = { foo: "value" }; + await stateProvider.setUserState(SettingsKey, settings, SomeUser); const generator = new CredentialGeneratorService( randomizer, stateProvider, @@ -249,23 +239,15 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); - const generated = new ObservableTracker(generator.generate$(SomeConfiguration)); + const on$ = new BehaviorSubject({ source: "some source" }); + const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { on$ })); - await stateProvider.setUserState(SettingsKey, anotherSettings, SomeUser); - await generated.pauseUntilReceived(2); - generated.unsubscribe(); + const result = await generated.expectEmission(); - expect(generated.emissions).toEqual([ - new GeneratedCredential("some value", SomeAlgorithm, SomeTime), - new GeneratedCredential("another value", SomeAlgorithm, SomeTime), - ]); + expect(result.source).toEqual("some source"); }); - // FIXME: test these when the fake state provider can create the required emissions - it.todo("errors when the settings error"); - it.todo("completes when the settings complete"); - - it("includes `website$`'s last emitted value", async () => { + it("includes request.website in the generated credential", async () => { const settings = { foo: "value" }; await stateProvider.setUserState(SettingsKey, settings, SomeUser); const generator = new CredentialGeneratorService( @@ -277,18 +259,19 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); - const website$ = new BehaviorSubject("some website"); - const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { website$ })); + const on$ = new BehaviorSubject({ website: "some website" }); + const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { on$ })); const result = await generated.expectEmission(); - expect(result).toEqual( - new GeneratedCredential("some website|value", SomeAlgorithm, SomeTime), - ); + expect(result.website).toEqual("some website"); }); - it("errors when `website$` errors", async () => { - await stateProvider.setUserState(SettingsKey, null, SomeUser); + it("uses the active user's settings", async () => { + const someSettings = { foo: "some value" }; + const anotherSettings = { foo: "another value" }; + await stateProvider.setUserState(SettingsKey, someSettings, SomeUser); + await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser); const generator = new CredentialGeneratorService( randomizer, stateProvider, @@ -298,44 +281,23 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); - const website$ = new BehaviorSubject("some website"); - let error = null; + const on$ = new BehaviorSubject({}); + const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { on$ })); - generator.generate$(SomeConfiguration, { website$ }).subscribe({ - error: (e: unknown) => { - error = e; - }, - }); - website$.error({ some: "error" }); - await awaitAsync(); + await accountService.switchAccount(AnotherUser); + on$.next({}); + await generated.pauseUntilReceived(2); + generated.unsubscribe(); - expect(error).toEqual({ some: "error" }); + expect(generated.emissions).toEqual([ + new GeneratedCredential("some value", SomeAlgorithm, SomeTime), + new GeneratedCredential("another value", SomeAlgorithm, SomeTime), + ]); }); - it("completes when `website$` completes", async () => { - await stateProvider.setUserState(SettingsKey, null, SomeUser); - const generator = new CredentialGeneratorService( - randomizer, - stateProvider, - policyService, - apiService, - i18nService, - encryptorProvider, - accountService, - ); - const website$ = new BehaviorSubject("some website"); - let completed = false; - - generator.generate$(SomeConfiguration, { website$ }).subscribe({ - complete: () => { - completed = true; - }, - }); - website$.complete(); - await awaitAsync(); - - expect(completed).toBeTruthy(); - }); + // FIXME: test these when the fake state provider can create the required emissions + it.todo("errors when the settings error"); + it.todo("completes when the settings complete"); it("emits a generation for a specific user when `user$` supplied", async () => { await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); @@ -350,38 +312,17 @@ describe("CredentialGeneratorService", () => { accountService, ); const userId$ = new BehaviorSubject(AnotherUser).asObservable(); - const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { userId$ })); + const on$ = new Subject(); + const generated = new ObservableTracker( + generator.generate$(SomeConfiguration, { on$, userId$ }), + ); + on$.next({}); const result = await generated.expectEmission(); expect(result).toEqual(new GeneratedCredential("another", SomeAlgorithm, SomeTime)); }); - it("emits a generation for a specific user when `user$` emits", async () => { - await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); - await stateProvider.setUserState(SettingsKey, { foo: "another" }, AnotherUser); - const generator = new CredentialGeneratorService( - randomizer, - stateProvider, - policyService, - apiService, - i18nService, - encryptorProvider, - accountService, - ); - const userId = new BehaviorSubject(SomeUser); - const userId$ = userId.pipe(filter((u) => !!u)); - const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { userId$ })); - - userId.next(AnotherUser); - const result = await generated.pauseUntilReceived(2); - - expect(result).toEqual([ - new GeneratedCredential("value", SomeAlgorithm, SomeTime), - new GeneratedCredential("another", SomeAlgorithm, SomeTime), - ]); - }); - it("errors when `user$` errors", async () => { await stateProvider.setUserState(SettingsKey, null, SomeUser); const generator = new CredentialGeneratorService( @@ -393,10 +334,11 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); + const on$ = new Subject(); const userId$ = new BehaviorSubject(SomeUser); let error = null; - generator.generate$(SomeConfiguration, { userId$ }).subscribe({ + generator.generate$(SomeConfiguration, { on$, userId$ }).subscribe({ error: (e: unknown) => { error = e; }, @@ -418,10 +360,11 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); + const on$ = new Subject(); const userId$ = new BehaviorSubject(SomeUser); let completed = false; - generator.generate$(SomeConfiguration, { userId$ }).subscribe({ + generator.generate$(SomeConfiguration, { on$, userId$ }).subscribe({ complete: () => { completed = true; }, @@ -444,7 +387,7 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); - const on$ = new Subject(); + const on$ = new Subject(); const results: any[] = []; // confirm no emission during subscription @@ -455,7 +398,7 @@ describe("CredentialGeneratorService", () => { expect(results.length).toEqual(0); // confirm forwarded emission - on$.next(); + on$.next({}); await awaitAsync(); expect(results).toEqual([new GeneratedCredential("value", SomeAlgorithm, SomeTime)]); @@ -465,7 +408,7 @@ describe("CredentialGeneratorService", () => { expect(results.length).toBe(1); // confirm forwarded emission takes latest value - on$.next(); + on$.next({}); await awaitAsync(); sub.unsubscribe(); @@ -486,7 +429,7 @@ describe("CredentialGeneratorService", () => { encryptorProvider, accountService, ); - const on$ = new Subject(); + const on$ = new Subject(); let error: any = null; // confirm no emission during subscription @@ -501,35 +444,8 @@ describe("CredentialGeneratorService", () => { expect(error).toEqual({ some: "error" }); }); - it("completes when `on$` completes", async () => { - await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); - const generator = new CredentialGeneratorService( - randomizer, - stateProvider, - policyService, - apiService, - i18nService, - encryptorProvider, - accountService, - ); - const on$ = new Subject(); - let complete = false; - - // confirm no emission during subscription - generator.generate$(SomeConfiguration, { on$ }).subscribe({ - complete: () => { - complete = true; - }, - }); - on$.complete(); - await awaitAsync(); - - expect(complete).toBeTruthy(); - }); - // FIXME: test these when the fake state provider can delay its first emission it.todo("emits when settings$ become available if on$ is called before they're ready."); - it.todo("emits when website$ become available if on$ is called before they're ready."); }); describe("algorithms", () => { diff --git a/libs/tools/generator/core/src/services/credential-generator.service.ts b/libs/tools/generator/core/src/services/credential-generator.service.ts index 9659076ec0c..6e80049870c 100644 --- a/libs/tools/generator/core/src/services/credential-generator.service.ts +++ b/libs/tools/generator/core/src/services/credential-generator.service.ts @@ -2,20 +2,15 @@ // @ts-strict-ignore import { BehaviorSubject, - combineLatest, - concat, concatMap, distinctUntilChanged, endWith, filter, - first, firstValueFrom, ignoreElements, map, Observable, ReplaySubject, - share, - skipUntil, switchMap, takeUntil, withLatestFrom, @@ -34,9 +29,9 @@ import { SingleUserDependency, UserDependency, } from "@bitwarden/common/tools/dependencies"; -import { IntegrationId, IntegrationMetadata } from "@bitwarden/common/tools/integration"; +import { IntegrationMetadata } from "@bitwarden/common/tools/integration"; import { RestClient } from "@bitwarden/common/tools/integration/rpc"; -import { anyComplete } from "@bitwarden/common/tools/rx"; +import { anyComplete, withLatestReady } from "@bitwarden/common/tools/rx"; import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject"; import { UserId } from "@bitwarden/common/types/guid"; @@ -57,6 +52,7 @@ import { CredentialPreference, isForwarderIntegration, ForwarderIntegration, + GenerateRequest, } from "../types"; import { CredentialGeneratorConfiguration as Configuration, @@ -69,19 +65,7 @@ import { PREFERENCES } from "./credential-preferences"; type Policy$Dependencies = UserDependency; type Settings$Dependencies = Partial; -type Generate$Dependencies = Simplify & Partial> & { - /** Emits the active website when subscribed. - * - * The generator does not respond to emissions of this interface; - * If it is provided, the generator blocks until a value becomes available. - * When `website$` is omitted, the generator uses the empty string instead. - * When `website$` completes, the generator completes. - * When `website$` errors, the generator forwards the error. - */ - website$?: Observable; - - integration$?: Observable; -}; +type Generate$Dependencies = Simplify & Partial>; type Algorithms$Dependencies = Partial; @@ -111,43 +95,20 @@ export class CredentialGeneratorService { /** Generates a stream of credentials * @param configuration determines which generator's settings are loaded - * @param dependencies.on$ when specified, a new credential is emitted when - * this emits. Otherwise, a new credential is emitted when the settings - * update. + * @param dependencies.on$ Required. A new credential is emitted when this emits. */ generate$( configuration: Readonly>, - dependencies?: Generate$Dependencies, + dependencies: Generate$Dependencies, ) { - // instantiate the engine const engine = configuration.engine.create(this.getDependencyProvider()); - - // stream blocks until all of these values are received - const website$ = dependencies?.website$ ?? new BehaviorSubject(null); - const request$ = website$.pipe(map((website) => ({ website }))); const settings$ = this.settings$(configuration, dependencies); - // if on$ triggers before settings are loaded, trigger as soon - // as they become available. - let readyOn$: Observable = null; - if (dependencies?.on$) { - const NO_EMISSIONS = {}; - const ready$ = combineLatest([settings$, request$]).pipe( - first(null, NO_EMISSIONS), - filter((value) => value !== NO_EMISSIONS), - share(), - ); - readyOn$ = concat( - dependencies.on$?.pipe(switchMap(() => ready$)), - dependencies.on$.pipe(skipUntil(ready$)), - ); - } - // generation proper - const generate$ = (readyOn$ ?? settings$).pipe( - withLatestFrom(request$, settings$), - concatMap(([, request, settings]) => engine.generate(request, settings)), - takeUntil(anyComplete([request$, settings$])), + const generate$ = dependencies.on$.pipe( + withLatestReady(settings$), + concatMap(([request, settings]) => engine.generate(request, settings)), + takeUntil(anyComplete([settings$])), ); return generate$; @@ -256,7 +217,8 @@ export class CredentialGeneratorService { category: generator.category, name: integration ? integration.name : this.i18nService.t(generator.nameKey), generate: this.i18nService.t(generator.generateKey), - generatedValue: this.i18nService.t(generator.generatedValueKey), + onGeneratedMessage: this.i18nService.t(generator.onGeneratedMessageKey), + credentialType: this.i18nService.t(generator.credentialTypeKey), copy: this.i18nService.t(generator.copyKey), useGeneratedValue: this.i18nService.t(generator.useGeneratedValueKey), onlyOnRequest: generator.onlyOnRequest, diff --git a/libs/tools/generator/core/src/types/credential-generator-configuration.ts b/libs/tools/generator/core/src/types/credential-generator-configuration.ts index 0650ae5d34d..08aec48a9e7 100644 --- a/libs/tools/generator/core/src/types/credential-generator-configuration.ts +++ b/libs/tools/generator/core/src/types/credential-generator-configuration.ts @@ -34,6 +34,9 @@ export type AlgorithmInfo = { /* Localized generate button label */ generate: string; + /** Localized "credential generated" informational message */ + onGeneratedMessage: string; + /* Localized copy button label */ copy: string; @@ -41,7 +44,7 @@ export type AlgorithmInfo = { useGeneratedValue: string; /* Localized generated value label */ - generatedValue: string; + credentialType: string; /** Localized algorithm description */ description?: string; @@ -79,17 +82,22 @@ export type CredentialGeneratorInfo = { /** Localization key for the credential description*/ descriptionKey?: string; - /* Localization key for the generate command label */ + /** Localization key for the generate command label */ generateKey: string; - /* Localization key for the copy button label */ + /** Localization key for the copy button label */ copyKey: string; - /* Localized "use generated credential" button label */ + /** Localization key for the "credential generated" informational message */ + onGeneratedMessageKey: string; + + /** Localized "use generated credential" button label */ useGeneratedValueKey: string; - /* Localization key for describing values generated by this generator */ - generatedValueKey: string; + /** Localization key for describing the kind of credential generated + * by this generator. + */ + credentialTypeKey: string; /** When true, credential generation must be explicitly requested. * @remarks this property is useful when credential generation diff --git a/libs/tools/generator/core/src/types/credential-generator.ts b/libs/tools/generator/core/src/types/credential-generator.ts index c95ff25afff..c421bbbff87 100644 --- a/libs/tools/generator/core/src/types/credential-generator.ts +++ b/libs/tools/generator/core/src/types/credential-generator.ts @@ -1,5 +1,4 @@ -import { GenerationRequest } from "@bitwarden/common/tools/types"; - +import { GenerateRequest } from "./generate-request"; import { GeneratedCredential } from "./generated-credential"; /** An algorithm that generates credentials. */ @@ -8,5 +7,5 @@ export type CredentialGenerator = { * @param request runtime parameters * @param settings stored parameters */ - generate: (request: GenerationRequest, settings: Settings) => Promise; + generate: (request: GenerateRequest, settings: Settings) => Promise; }; diff --git a/libs/tools/generator/core/src/types/generate-request.ts b/libs/tools/generator/core/src/types/generate-request.ts new file mode 100644 index 00000000000..c7d5bf9c41c --- /dev/null +++ b/libs/tools/generator/core/src/types/generate-request.ts @@ -0,0 +1,24 @@ +/** Contextual information about the application state when a generator is invoked. + */ +export type GenerateRequest = { + /** Traces the origin of the generation request. This parameter is + * copied to the generated credential. + * + * @remarks This parameter it is provided solely so that generator + * consumers can differentiate request sources from one another. + * It never affects the random output of the generator algorithms, + * and it is never communicated to 3rd party systems. It MAY be + * tracked in the generator history. + */ + source?: string; + + /** Traces the website associated with a generated credential. + * + * @remarks Random generators MUST NOT depend upon the website during credential + * generation. Non-random generators MAY include the website in the generated + * credential (e.g. a catchall email address). This parameter MAY be transmitted + * to 3rd party systems (e.g. as the description for a forwarding email). + * It MAY be tracked in the generator history. + */ + website?: string; +}; diff --git a/libs/tools/generator/core/src/types/generated-credential.ts b/libs/tools/generator/core/src/types/generated-credential.ts index 6d18a1c7892..99b864b9fd8 100644 --- a/libs/tools/generator/core/src/types/generated-credential.ts +++ b/libs/tools/generator/core/src/types/generated-credential.ts @@ -11,11 +11,15 @@ export class GeneratedCredential { * @param generationDate The date that the credential was generated. * Numeric values should are interpreted using {@link Date.valueOf} * semantics. + * @param source traces the origin of the request that generated this credential. + * @param website traces the website associated with the generated credential. */ constructor( readonly credential: string, readonly category: CredentialAlgorithm, generationDate: Date | number, + readonly source?: string, + readonly website?: string, ) { if (typeof generationDate === "number") { this.generationDate = new Date(generationDate); @@ -25,7 +29,7 @@ export class GeneratedCredential { } /** The date that the credential was generated */ - generationDate: Date; + readonly generationDate: Date; /** Constructs a credential from its `toJSON` representation */ static fromJSON(jsonValue: Jsonify) { @@ -38,6 +42,9 @@ export class GeneratedCredential { /** Serializes a credential to a JSON-compatible object */ toJSON() { + // omits the source and website because they were introduced to solve + // UI bugs and it's not yet known whether there's a desire to support + // them in the generator history view. return { credential: this.credential, category: this.category, diff --git a/libs/tools/generator/core/src/types/index.ts b/libs/tools/generator/core/src/types/index.ts index 48272cbf602..3e392257b0c 100644 --- a/libs/tools/generator/core/src/types/index.ts +++ b/libs/tools/generator/core/src/types/index.ts @@ -6,6 +6,7 @@ export * from "./credential-generator"; export * from "./credential-generator-configuration"; export * from "./eff-username-generator-options"; export * from "./forwarder-options"; +export * from "./generate-request"; export * from "./generator-constraints"; export * from "./generated-credential"; export * from "./generator-options"; diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts index 636b7546af8..4b569532220 100644 --- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts +++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts @@ -4,11 +4,13 @@ import { CommonModule } from "@angular/common"; import { Component, Input, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; -import { firstValueFrom, map } from "rxjs"; +import { BehaviorSubject, firstValueFrom, map, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; @@ -25,7 +27,7 @@ import { ToastService, TypographyModule, } from "@bitwarden/components"; -import { CredentialGeneratorService, Generators } from "@bitwarden/generator-core"; +import { CredentialGeneratorService, GenerateRequest, Generators } from "@bitwarden/generator-core"; import { SendFormConfig } from "../../abstractions/send-form-config.service"; import { SendFormContainer } from "../../send-form-container"; @@ -89,11 +91,14 @@ export class SendOptionsComponent implements OnInit { private i18nService: I18nService, private toastService: ToastService, private generatorService: CredentialGeneratorService, + private accountService: AccountService, ) { this.sendFormContainer.registerChildForm("sendOptionsForm", this.sendOptionsForm); - this.policyService - .getAll$(PolicyType.SendOptions) + + this.accountService.activeAccount$ .pipe( + getUserId, + switchMap((userId) => this.policyService.getAll$(PolicyType.SendOptions, userId)), map((policies) => policies?.some((p) => p.data.disableHideEmail)), takeUntilDestroyed(), ) @@ -116,8 +121,9 @@ export class SendOptionsComponent implements OnInit { } generatePassword = async () => { + const on$ = new BehaviorSubject({ source: "send" }); const generatedCredential = await firstValueFrom( - this.generatorService.generate$(Generators.password), + this.generatorService.generate$(Generators.password, { on$ }), ); this.sendOptionsForm.patchValue({ diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts index ee1f27b712d..26f967e4a53 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts @@ -157,8 +157,11 @@ describe("ItemDetailsSectionComponent", () => { }); describe("allowOwnershipChange", () => { - it("should not allow ownership change in edit mode", () => { + it("should not allow ownership change if in edit mode and the cipher is owned by an organization", () => { component.config.mode = "edit"; + component.originalCipherView = { + organizationId: "org1", + } as CipherView; expect(component.allowOwnershipChange).toBe(false); }); @@ -195,6 +198,7 @@ describe("ItemDetailsSectionComponent", () => { it("should show personal ownership when the configuration allows", () => { component.config.mode = "edit"; component.config.allowPersonalOwnership = true; + component.originalCipherView = {} as CipherView; component.config.organizations = [{ id: "134-433-22" } as Organization]; fixture.detectChanges(); @@ -208,6 +212,7 @@ describe("ItemDetailsSectionComponent", () => { it("should show personal ownership when the control is disabled", async () => { component.config.mode = "edit"; component.config.allowPersonalOwnership = false; + component.originalCipherView = {} as CipherView; component.config.organizations = [{ id: "134-433-22" } as Organization]; await component.ngOnInit(); fixture.detectChanges(); diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts index a01d25f0600..e6799c54cb0 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts @@ -150,8 +150,8 @@ export class ItemDetailsSectionComponent implements OnInit { } get allowOwnershipChange() { - // Do not allow ownership change in edit mode. - if (this.config.mode === "edit") { + // Do not allow ownership change in edit mode and the cipher is owned by an organization + if (this.config.mode === "edit" && this.originalCipherView.organizationId != null) { return false; } diff --git a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts index 92a28f9b15f..059214cc185 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts @@ -63,8 +63,16 @@ export class DefaultCipherFormService implements CipherFormService { const originalCollectionIds = new Set(config.originalCipher.collectionIds ?? []); const newCollectionIds = new Set(cipher.collectionIds ?? []); - // If the collectionIds are the same, update the cipher normally - if (isSetEqual(originalCollectionIds, newCollectionIds)) { + // Call shareWithServer if the owner is changing from a user to an organization + if (config.originalCipher.organizationId === null && cipher.organizationId != null) { + savedCipher = await this.cipherService.shareWithServer( + cipher, + cipher.organizationId, + cipher.collectionIds, + activeUserId, + ); + // If the collectionIds are the same, update the cipher normally + } else if (isSetEqual(originalCollectionIds, newCollectionIds)) { savedCipher = await this.cipherService.updateWithServer(encryptedCipher, config.admin); } else { // Updating a cipher with collection changes is not supported with a single request currently diff --git a/package-lock.json b/package-lock.json index 22565c93c7b..67123e09249 100644 --- a/package-lock.json +++ b/package-lock.json @@ -134,7 +134,7 @@ "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", "css-loader": "7.1.2", - "electron": "33.3.1", + "electron": "34.0.0", "electron-builder": "24.13.3", "electron-log": "5.2.4", "electron-reload": "2.0.0-alpha.1", @@ -192,11 +192,11 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2025.1.1" + "version": "2025.1.4" }, "apps/cli": { "name": "@bitwarden/cli", - "version": "2025.1.1", + "version": "2025.1.3", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@koa/multer": "3.0.2", @@ -232,7 +232,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2025.1.4", + "version": "2025.1.8", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -246,7 +246,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2025.1.1" + "version": "2025.1.2" }, "libs/admin-console": { "name": "@bitwarden/admin-console", @@ -10341,7 +10341,6 @@ "integrity": "sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/types": "8.20.0", "@typescript-eslint/visitor-keys": "8.20.0", @@ -10369,7 +10368,6 @@ "integrity": "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.20.0", @@ -10394,7 +10392,6 @@ "integrity": "sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/types": "8.20.0", "eslint-visitor-keys": "^4.2.0" @@ -15208,9 +15205,9 @@ } }, "node_modules/electron": { - "version": "33.3.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-33.3.1.tgz", - "integrity": "sha512-Z7l2bVgpdKxHQMI4i0CirBX2n+iCYKOx5mbzNM3BpOyFELwlobEXKmzCmEnwP+3EcNeIhUQyIEBFQxN06QgdIw==", + "version": "34.0.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-34.0.0.tgz", + "integrity": "sha512-fpaPb0lifoUJ6UJa4Lk8/0B2Ku/xDZWdc1Gkj67jbygTCrvSon0qquju6Ltx1Kz23GRqqlIHXiy9EvrjpY7/Wg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -15436,9 +15433,9 @@ } }, "node_modules/electron/node_modules/@types/node": { - "version": "20.17.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.12.tgz", - "integrity": "sha512-vo/wmBgMIiEA23A/knMfn/cf37VnuF52nZh5ZoW0GWt4e4sxNquibrMRJ7UQsA06+MBx9r/H1jsI9grYjQCQlw==", + "version": "20.17.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.14.tgz", + "integrity": "sha512-w6qdYetNL5KRBiSClK/KWai+2IMEJuAj+EujKCumalFOwXtvOXaEan9AuwcRID2IcOIAWSIfR495hBtgKlx2zg==", "dev": true, "license": "MIT", "dependencies": { @@ -30581,7 +30578,6 @@ "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=18.12" }, diff --git a/package.json b/package.json index b1cae8d01a8..895f1d844c9 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", "css-loader": "7.1.2", - "electron": "33.3.1", + "electron": "34.0.0", "electron-builder": "24.13.3", "electron-log": "5.2.4", "electron-reload": "2.0.0-alpha.1",