diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index efc8c25fc5e..b50db6e08b6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -100,13 +100,13 @@ jobs: persist-credentials: false - name: Install Rust - uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable + uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # stable with: toolchain: stable components: rustfmt, clippy - name: Install Rust nightly - uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable + uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # stable with: toolchain: nightly components: rustfmt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a0f783bbb36..c2fd4b7c32b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -210,7 +210,7 @@ jobs: persist-credentials: false - name: Install rust - uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable + uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # stable with: toolchain: stable components: llvm-tools diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 78cf90c3555..7334362d446 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 6a43475da32..6168f7cf2dd 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -573,13 +573,10 @@ "noItemsInArchiveDesc": { "message": "Arxivlənmiş elementlər burada görünəcək, ümumi axtarış nəticələrindən və avto-doldurma təkliflərindən xaric ediləcək." }, - "itemWasSentToArchive": { - "message": "Element arxivə göndərildi" + "itemArchiveToast": { + "message": "Element arxivləndi" }, - "itemWasUnarchived": { - "message": "Element arxivdən çıxarıldı" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "Element arxivdən çıxarıldı" }, "archiveItem": { diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 9f4a65e3072..ea569cabdf4 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index a46ad75065e..e5d68bce366 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Архивираните елементи ще се показват тук и ще бъдат изключени от общите резултати при търсене и от предложенията за автоматично попълване." }, - "itemWasSentToArchive": { - "message": "Елементът беше преместен в архива" + "itemArchiveToast": { + "message": "Елементът е преместен в архива" }, - "itemWasUnarchived": { - "message": "Елементът беше изваден от архива" - }, - "itemUnarchived": { - "message": "Елементът беше изваден от архива" + "itemUnarchivedToast": { + "message": "Елементът е изваден от архива" }, "archiveItem": { "message": "Архивиране на елемента" diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index b46d0664231..533b12ab0a5 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index e81fc637b5c..35c4177e5eb 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 2bd53876953..8e82fc34be4 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 1501c7d7c4a..ed1b37134e1 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -573,13 +573,10 @@ "noItemsInArchiveDesc": { "message": "Zde se zobrazí archivované položky a budou vyloučeny z obecných výsledků vyhledávání a návrhů automatického vyplňování." }, - "itemWasSentToArchive": { - "message": "Položka byla přesunuta do archivu" + "itemArchiveToast": { + "message": "Položka archivována" }, - "itemWasUnarchived": { - "message": "Položka byla odebrána z archivu" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "Položka byla odebrána z archivu" }, "archiveItem": { diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 6910fe2efb3..165cd05de8e 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index faf4fc855ec..615cc6a2a0b 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index ad5b45159df..8f2b023bc00 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archivierte Einträge werden hier angezeigt und von allgemeinen Suchergebnissen sowie Vorschlägen zum automatischen Ausfüllen ausgeschlossen." }, - "itemWasSentToArchive": { - "message": "Eintrag wurde archiviert" + "itemArchiveToast": { + "message": "Eintrag archiviert" }, - "itemWasUnarchived": { - "message": "Eintrag wird nicht mehr archiviert" - }, - "itemUnarchived": { - "message": "Eintrag wird nicht mehr archiviert" + "itemUnarchivedToast": { + "message": "Eintrag nicht mehr archiviert" }, "archiveItem": { "message": "Eintrag archivieren" @@ -5964,7 +5961,7 @@ "message": "Kartennummer" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Fehler: Entschlüsselung nicht möglich" }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Deine Organisation verwendet keine Master-Passwörter mehr, um sich bei Bitwarden anzumelden. Verifiziere die Organisation und Domain, um fortzufahren." @@ -6128,7 +6125,7 @@ "message": "benutzer@bitwarden.com, benutzer@acme.com" }, "downloadBitwardenApps": { - "message": "Download Bitwarden apps" + "message": "Bitwarden-Apps herunterladen" }, "emailProtected": { "message": "E-Mail-Adresse geschützt" diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 59f757008f2..68f7267825d 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Το στοιχείο στάλθηκε στην αρχειοθήκη" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Το στοιχείο επαναφέρθηκε από την αρχειοθήκη" - }, - "itemUnarchived": { - "message": "Το στοιχείο επαναφέρθηκε από την αρχειοθήκη" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Αρχειοθέτηση στοιχείου" diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 7944904c44a..a221dc4f338 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -3083,6 +3080,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index e34e20844e6..d61774df145 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 9fd388a80d3..3622ffce241 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index ab5fad7e3af..131263ea4d9 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Los elementos archivados aparecerán aquí y se excluirán de los resultados de búsqueda generales y de sugerencias de autocompletado." }, - "itemWasSentToArchive": { - "message": "El elemento fue archivado" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "El elemento fue desarchivado" - }, - "itemUnarchived": { - "message": "El elemento fue desarchivado" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archivar elemento" @@ -6113,7 +6110,7 @@ "message": "Resize side navigation" }, "whoCanView": { - "message": "Quien puede ver" + "message": "Quién puede ver" }, "specificPeople": { "message": "Personas específicas" diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index e8efd12b1e2..cd78c444c89 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index e7fcd4998e0..3e4382a3d3b 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -3,14 +3,14 @@ "message": "Bitwarden" }, "appLogoLabel": { - "message": "Bitwarden logo" + "message": "Bitwardenen logoa" }, "extName": { "message": "Bitwarden pasahitz kudeatzailea", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Etxean, lanean edo bidean, Bitwardenek zure pasahitz, giltz orokor edo informazio delikatua erraz gordetzen du", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -23,19 +23,19 @@ "message": "Sortu kontua" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Berria Bitwardenen?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Sartu giltz orokorrarekin" }, "unlockWithPasskey": { - "message": "Unlock with passkey" + "message": "Ireki giltz orokorrarekin" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "Erabili SSO" }, "yourOrganizationRequiresSingleSignOn": { - "message": "Your organization requires single sign-on." + "message": "Zure erakundeak SSO erabiltzera behartzen du." }, "welcomeBack": { "message": "Ongi etorri berriro ere" @@ -71,7 +71,7 @@ "message": "Pasahitz nagusia ahazten baduzu, pista batek pasahitza gogoratzen lagunduko dizu." }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Zure pasahitza ahazten bazaizu, pasahitzaren pista emailez bidal dezakegu. Gehienez $CURRENT$/$MAXIMUM$ karaktere.", "placeholders": { "current": { "content": "$1", @@ -90,7 +90,7 @@ "message": "Pasahitz nagusirako pista (aukerakoa)" }, "passwordStrengthScore": { - "message": "Password strength score $SCORE$", + "message": "Pasahitzaren sendotasun puntuazioa $SCORE$", "placeholders": { "score": { "content": "$1", @@ -99,10 +99,10 @@ } }, "joinOrganization": { - "message": "Join organization" + "message": "Erakundearen kide egin" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "$ORGANIZATIONNAME$-ren kide egin", "placeholders": { "organizationName": { "content": "$1", @@ -111,7 +111,7 @@ } }, "finishJoiningThisOrganizationBySettingAMasterPassword": { - "message": "Finish joining this organization by setting a master password." + "message": "Bukatu erakunde honen kide egitea pasahitz nagusi bat ezarriz." }, "tab": { "message": "Fitxak" @@ -138,7 +138,7 @@ "message": "Kopiatu pasahitza" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "Kopiatu esaldi-gakoa" }, "copyNote": { "message": "Kopiatu oharra" @@ -159,28 +159,28 @@ "message": "Izena kopiatu" }, "copyCompany": { - "message": "Copy company" + "message": "Kopiatu enpresa" }, "copySSN": { - "message": "Copy Social Security number" + "message": "Kopiatu segurtasun sozialaren zenbakia" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "Kopiatu pasaporte zenbakia" }, "copyLicenseNumber": { - "message": "Copy license number" + "message": "Kopiatu lizentzia zenbakia" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "Kopiatu gako pribatua" }, "copyPublicKey": { - "message": "Copy public key" + "message": "Kopiatu gako publikoa" }, "copyFingerprint": { - "message": "Copy fingerprint" + "message": "Kopiatu hatz-marka" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "Kopiatu $FIELD$", "placeholders": { "field": { "content": "$1", @@ -189,7 +189,7 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Kopiatu webgunea" }, "copyNotes": { "message": "Kopiatu oharrak" @@ -206,7 +206,7 @@ "message": "Auto-betetzea" }, "autoFillLogin": { - "message": "Autofill login" + "message": "Saio-hasiera autobetetzea" }, "autoFillCard": { "message": "Auto-bete txartela" @@ -261,16 +261,16 @@ "message": "Gehitu elementua" }, "accountEmail": { - "message": "Account email" + "message": "Kontuaren e-maila" }, "requestHint": { - "message": "Request hint" + "message": "Argibidea eskatu" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "Pasahitz-laguntza eskatu" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { - "message": "Enter your account email address and your password hint will be sent to you" + "message": "Idatzi zure kontuaren e-maila eta pasahitzaren argibidea bidaliko dizugu" }, "getMasterPasswordHint": { "message": "Jaso pasahitz nagusiaren pista" @@ -297,10 +297,10 @@ "message": "Aldatu pasahitz nagusia" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Web aplikaziora jarraitu?" }, "continueToWebAppDesc": { - "message": "Explore more features of your Bitwarden account on the web app." + "message": "Esploratu zure Bitwarden kontuaren funtzio gehiago web-aplikazioan." }, "continueToHelpCenter": { "message": "Continue to Help Center?" @@ -332,7 +332,7 @@ "message": "Itxi saioa" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "Bitwardeni buruz" }, "about": { "message": "Honi buruz" @@ -398,10 +398,10 @@ } }, "newFolder": { - "message": "New folder" + "message": "Karpeta berria" }, "folderName": { - "message": "Folder name" + "message": "Karpetaren izena" }, "folderHintText": { "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" @@ -440,7 +440,7 @@ "message": "Sinkronizatu" }, "syncNow": { - "message": "Sync now" + "message": "Sinkronizatu orain" }, "lastSync": { "message": "Azken sinkronizazioa:" @@ -456,7 +456,7 @@ "message": "Automatikoki pasahitz sendo eta bakarrak sortzen ditu zure saio-hasieratarako." }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "Bitwarden web aplikazioa" }, "select": { "message": "Hautatu" @@ -489,11 +489,11 @@ "message": "Luzera" }, "include": { - "message": "Include", + "message": "Sartu", "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "Sartu letra maiuskulak", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -501,7 +501,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "Sartu letra minuskulak", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -509,7 +509,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "Sartu zenbakiak", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -517,7 +517,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "Sartu karaktere bereziak", "description": "Full description for the password generator special characters checkbox" }, "numWords": { @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -607,16 +604,16 @@ "message": "Erakutsi" }, "viewAll": { - "message": "View all" + "message": "Ikusi denak" }, "showAll": { - "message": "Show all" + "message": "Dena erakutsi" }, "viewLess": { "message": "View less" }, "viewLogin": { - "message": "View login" + "message": "Ikusi saio-hasiera" }, "noItemsInList": { "message": "Ez dago erakusteko elementurik." @@ -946,16 +943,16 @@ "message": "Saioa amaitu da." }, "logIn": { - "message": "Log in" + "message": "Hasi saioa" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Sartu Bitwardenera" }, "enterTheCodeSentToYourEmail": { - "message": "Enter the code sent to your email" + "message": "Sartu e-mailera bidali dizugun kodea" }, "enterTheCodeFromYourAuthenticatorApp": { - "message": "Enter the code from your authenticator app" + "message": "Sartu zure egiaztapenerako aplikazioko kodea" }, "pressYourYubiKeyToAuthenticate": { "message": "Press your YubiKey to authenticate" @@ -1344,11 +1341,11 @@ "message": "Export from" }, "exportVerb": { - "message": "Export", + "message": "Esportatu", "description": "The verb form of the word Export" }, "exportNoun": { - "message": "Export", + "message": "Esportatu", "description": "The noun form of the word Export" }, "importNoun": { @@ -1768,7 +1765,7 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Autofill suggestions" + "message": "Autobetetzeko iradokizunak" }, "autofillSpotlightTitle": { "message": "Easily find autofill suggestions" @@ -2165,7 +2162,7 @@ "description": "Header for edit file send" }, "viewItemHeaderLogin": { - "message": "View Login", + "message": "Ikusi saio-hasiera", "description": "Header for view login item type" }, "viewItemHeaderCard": { @@ -2203,7 +2200,7 @@ "message": "Bildumak" }, "nCollections": { - "message": "$COUNT$ collections", + "message": "$COUNT$ bilduma", "placeholders": { "count": { "content": "$1", @@ -2928,7 +2925,7 @@ } }, "send": { - "message": "Send", + "message": "Bidali", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDetails": { @@ -4019,7 +4016,7 @@ "message": "required" }, "search": { - "message": "Search" + "message": "Bilatu" }, "inputMinLength": { "message": "Input must be at least $COUNT$ characters long.", @@ -4346,7 +4343,7 @@ "message": "Select a folder" }, "selectImportCollection": { - "message": "Select a collection" + "message": "Bilduma bat aukeratu" }, "importTargetHintCollection": { "message": "Select this option if you want the imported file contents moved to a collection" @@ -4525,7 +4522,7 @@ "message": "Try again or look for an email from LastPass to verify it's you." }, "collection": { - "message": "Collection" + "message": "Bilduma" }, "lastPassYubikeyDesc": { "message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button." @@ -4686,7 +4683,7 @@ "message": "Passkey removed" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "Autobetetzeko proposamenak" }, "itemSuggestions": { "message": "Suggested items" @@ -4812,7 +4809,7 @@ "message": "No values to copy" }, "assignToCollections": { - "message": "Assign to collections" + "message": "Esleitu bildumetan" }, "copyEmail": { "message": "Copy email" @@ -4904,7 +4901,7 @@ } }, "new": { - "message": "New" + "message": "Berria" }, "removeItem": { "message": "Remove $NAME$", @@ -4942,10 +4939,10 @@ "message": "Additional information" }, "itemHistory": { - "message": "Item history" + "message": "Aldaketen historia" }, "lastEdited": { - "message": "Last edited" + "message": "Azken edizioa" }, "ownerYou": { "message": "Owner: You" @@ -5029,7 +5026,7 @@ "message": "Filters" }, "filterVault": { - "message": "Filter vault" + "message": "Iragazi kutxa gotorra" }, "filterApplied": { "message": "One filter applied" @@ -5066,13 +5063,13 @@ "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." }, "loginCredentials": { - "message": "Login credentials" + "message": "Saio-hasierako kredentzialak" }, "authenticatorKey": { "message": "Authenticator key" }, "autofillOptions": { - "message": "Autofill options" + "message": "Autobetetzeko aukerak" }, "websiteUri": { "message": "Website (URI)" @@ -5866,7 +5863,7 @@ "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "generatorNudgeTitle": { - "message": "Quickly create passwords" + "message": "Sortu pasahitzak azkar" }, "generatorNudgeBodyOne": { "message": "Easily create strong and unique passwords by clicking on", @@ -5879,7 +5876,7 @@ "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." }, "generatorNudgeBodyAria": { - "message": "Easily create strong and unique passwords by clicking on the Generate password button to help you keep your logins secure.", + "message": "Sortu erraz pasahitz sendo eta bakarrak Sortu pasahitza botoian klik eginez, zure saio-hasierak seguru mantentzen laguntzeko.", "description": "Aria label for the body content of the generator nudge" }, "aboutThisSetting": { @@ -6107,7 +6104,7 @@ "message": "Items" }, "searchResults": { - "message": "Search results" + "message": "Bilaketaren emaitzak" }, "resizeSideNavigation": { "message": "Resize side navigation" diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index bca4ad20d52..5bb22dc6292 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 2997ed6c128..2f5b1ec4932 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arkistoi kohde" diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 11da450cc0f..abb06f0f19f 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index face33e0087..596315c4d3f 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Les éléments archivés apparaîtront ici et seront exclus des résultats de recherche généraux et des suggestions de remplissage automatique." }, - "itemWasSentToArchive": { - "message": "L'élément a été envoyé à l'archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "L'élément a été désarchivé" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archiver l'élément" diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 69ef54f78eb..e710a489f9a 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 22939259639..a76cbb711a9 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "פריטים בארכיון יופיעו כאן ויוחרגו מתוצאות חיפוש כללי והצעות למילוי אוטומטי." }, - "itemWasSentToArchive": { - "message": "הפריט נשלח לארכיון" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "הפריט שוחזר מהארכיב" - }, - "itemUnarchived": { - "message": "הפריט הוסר מהארכיון" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "העבר פריט לארכיון" diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 298f0312be7..d30cbd2cc6e 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index d7814a22da0..98fdee3b657 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Arhivirane stavke biti će prikazane ovdje i biti će izuzete iz rezultata općih pretraga i preporuka auto-ispune." }, - "itemWasSentToArchive": { - "message": "Stavka poslana u arhivu" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Stavka vraćena iz arhive" - }, - "itemUnarchived": { - "message": "Stavka vraćena iz arhive" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arhiviraj stavku" diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index ec4b2d405bf..e6765219f15 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -573,15 +573,12 @@ "noItemsInArchiveDesc": { "message": "Az archivált elemek itt jelennek meg és kizárásra kerülnek az általános keresési eredményekből és az automatikus kitöltési javaslatokból." }, - "itemWasSentToArchive": { - "message": "Az elem az archivumba került." + "itemArchiveToast": { + "message": "Az elem archiválásra került." }, - "itemWasUnarchived": { + "itemUnarchivedToast": { "message": "Az elem visszavételre került az archivumból." }, - "itemUnarchived": { - "message": "Az elemek visszavéelre kerültek az archivumból." - }, "archiveItem": { "message": "Elem archiválása" }, diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index f364b2f7540..ccf35569f36 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Butir yang diarsipkan akan muncul di sini dan akan dikecualikan dari hasil pencarian umum dan saran isi otomatis." }, - "itemWasSentToArchive": { - "message": "Butir dikirim ke arsip" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arsipkan butir" diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 9c4ce6a0369..42efa025207 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Gli elementi archiviati compariranno qui e saranno esclusi dai risultati di ricerca e suggerimenti di autoriempimento." }, - "itemWasSentToArchive": { - "message": "Elemento archiviato" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Elemento rimosso dall'archivio" - }, - "itemUnarchived": { - "message": "Elemento rimosso dall'archivio" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archivia elemento" diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index c8a963fc744..915308cec13 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "アーカイブされたアイテムはここに表示され、通常の検索結果および自動入力の候補から除外されます。" }, - "itemWasSentToArchive": { - "message": "アイテムはアーカイブに送信されました" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "アイテムはアーカイブから解除されました" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "アイテムをアーカイブ" diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index cb6129ed2bb..791664e6eec 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 336e8783b75..c28007c3838 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index e97ce2a95a4..faef7703a66 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 9f570d62abb..b4a04e75e43 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "보관된 항목은 여기에 표시되며 일반 검색 결과 및 자동 완성 제안에서 제외됩니다." }, - "itemWasSentToArchive": { - "message": "항목이 보관함으로 이동되었습니다" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "항목이 보관 해제되었습니다" - }, - "itemUnarchived": { - "message": "항목 보관 해제됨" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "항목 보관" diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 6e105f044f3..68eb11aa234 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 8c86d7040fe..6eaf545e390 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Šeit parādīsies arhivētie vienumi, un tie netiks iekļauti vispārējās meklēšanas iznākumos un automātiskās aizpildes ieteikumos." }, - "itemWasSentToArchive": { - "message": "Vienums tika ievietots arhīvā" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Vienums tika izņemts no arhīva" - }, - "itemUnarchived": { - "message": "Vienums tika izņemts no arhīva" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arhivēt vienumu" diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 61f69ffe22b..db48220ffbb 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 5cc614c5df7..abf2f7db968 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 336e8783b75..c28007c3838 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index ce6c8d5a7d4..4689cb23b7a 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 336e8783b75..c28007c3838 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 44522727429..044b3cfaa64 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Gearchiveerde items verschijnen hier en worden uitgesloten van algemene zoekresultaten en automatisch invulsuggesties." }, - "itemWasSentToArchive": { - "message": "Item naar archief verzonden" + "itemArchiveToast": { + "message": "Item gearchiveerd" }, - "itemWasUnarchived": { - "message": "Item uit het archief gehaald" - }, - "itemUnarchived": { - "message": "Item uit het archief gehaald" + "itemUnarchivedToast": { + "message": "Item gedearchiveerd" }, "archiveItem": { "message": "Item archiveren" diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 336e8783b75..c28007c3838 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 336e8783b75..c28007c3838 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 44c7d9e6d47..44c7b5fb6dd 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -440,7 +440,7 @@ "message": "Synchronizacja" }, "syncNow": { - "message": "Sync now" + "message": "Synchronizuj teraz" }, "lastSync": { "message": "Ostatnia synchronizacja:" @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Zarchiwizowane elementy pojawią się tutaj i zostaną wykluczone z wyników wyszukiwania i sugestii autouzupełniania." }, - "itemWasSentToArchive": { - "message": "Element został przeniesiony do archiwum" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Element został usunięty z archiwum" - }, - "itemUnarchived": { - "message": "Element został usunięty z archiwum" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archiwizuj element" @@ -6128,7 +6125,7 @@ "message": "user@bitwarden.com , user@acme.com" }, "downloadBitwardenApps": { - "message": "Download Bitwarden apps" + "message": "Pobierz aplikacje Bitwarden" }, "emailProtected": { "message": "Email protected" diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 5ad95b480db..679173205b1 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais de busca e das sugestões de preenchimento automático." }, - "itemWasSentToArchive": { - "message": "O item foi enviado para o arquivo" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "O item foi desarquivado" - }, - "itemUnarchived": { - "message": "O item foi desarquivado" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arquivar item" diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 604bf054707..9094e04094d 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais da pesquisa e das sugestões de preenchimento automático." }, - "itemWasSentToArchive": { - "message": "O item foi movido para o arquivo" + "itemArchiveToast": { + "message": "Item arquivado" }, - "itemWasUnarchived": { - "message": "O item foi desarquivado" - }, - "itemUnarchived": { - "message": "O item foi desarquivado" + "itemUnarchivedToast": { + "message": "Item desarquivado" }, "archiveItem": { "message": "Arquivar item" diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 12706943e83..47f7ae9cae3 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index dab9a22f03a..d1fb3de89a6 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Архивированные элементы появятся здесь и будут исключены из общих результатов поиска и предложений автозаполнения." }, - "itemWasSentToArchive": { - "message": "Элемент был отправлен в архив" + "itemArchiveToast": { + "message": "Элемент архивирован" }, - "itemWasUnarchived": { - "message": "Элемент был разархивирован" - }, - "itemUnarchived": { - "message": "Элемент был разархивирован" + "itemUnarchivedToast": { + "message": "Элемент разархивирован" }, "archiveItem": { "message": "Архивировать элемент" diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index d228cdb512a..e70c620eaf8 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index db7efcd8b9f..e1886098a31 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -573,13 +573,10 @@ "noItemsInArchiveDesc": { "message": "Tu sa zobrazia archivované položky, ktoré budú vylúčené zo všeobecného vyhľadávania a z návrhov automatického vypĺňania." }, - "itemWasSentToArchive": { + "itemArchiveToast": { "message": "Položka bola archivovaná" }, - "itemWasUnarchived": { - "message": "Položka bola odobraná z archívu" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "Položka bola odobraná z archívu" }, "archiveItem": { diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 07ee84ab810..100a04a3012 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 0ad71788514..e91e003c8e0 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Архивиране ставке ће се овде појавити и бити искључени из општих резултата претраге и сугестија о ауто-пуњењу." }, - "itemWasSentToArchive": { - "message": "Ставка је послата у архиву" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Ставка враћена из архиве" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Архивирај ставку" diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 08cec673d27..484817b0210 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Arkiverade objekt kommer att visas här och kommer att uteslutas från allmänna sökresultat och förslag för autofyll." }, - "itemWasSentToArchive": { - "message": "Objektet skickades till arkivet" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Objektet har avarkiverats" - }, - "itemUnarchived": { - "message": "Objektet har avarkiverats" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arkivera objekt" diff --git a/apps/browser/src/_locales/ta/messages.json b/apps/browser/src/_locales/ta/messages.json index 374c0968d2c..3e76c0ab0d1 100644 --- a/apps/browser/src/_locales/ta/messages.json +++ b/apps/browser/src/_locales/ta/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "காப்பகப்படுத்தப்பட்ட உருப்படிகள் இங்கே தோன்றும், மேலும் அவை பொதுவான தேடல் முடிவுகள் மற்றும் தானியங்குநிரப்பு பரிந்துரைகளிலிருந்து விலக்கப்படும்." }, - "itemWasSentToArchive": { - "message": "ஆவணம் காப்பகத்திற்கு அனுப்பப்பட்டது" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "காப்பகம் மீட்டெடுக்கப்பட்டது" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "உருப்படியைக் காப்பகப்படுத்து" diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 336e8783b75..c28007c3838 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 5af1c742f45..5ec728189a8 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "รายการที่จัดเก็บถาวรจะปรากฏที่นี่ และจะไม่ถูกรวมในผลการค้นหาทั่วไปหรือคำแนะนำการป้อนอัตโนมัติ" }, - "itemWasSentToArchive": { - "message": "ย้ายรายการไปที่จัดเก็บถาวรแล้ว" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "เลิกจัดเก็บถาวรรายการแล้ว" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "จัดเก็บรายการถาวร" diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 33f600fb7a7..7d5b31a9aba 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -573,13 +573,10 @@ "noItemsInArchiveDesc": { "message": "Arşivlenmiş kayıtlar burada görünecek ve genel arama sonuçlarından ile otomatik doldurma önerilerinden hariç tutulacaktır." }, - "itemWasSentToArchive": { - "message": "Kayıt arşive gönderildi" + "itemArchiveToast": { + "message": "Kayıt arşivlendi" }, - "itemWasUnarchived": { - "message": "Kayıt arşivden çıkarıldı" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "Kayıt arşivden çıkarıldı" }, "archiveItem": { diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index b703cfeefce..49a0c9de25b 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -573,13 +573,10 @@ "noItemsInArchiveDesc": { "message": "Архівовані записи з'являтимуться тут і будуть виключені з результатів звичайного пошуку та пропозицій автозаповнення." }, - "itemWasSentToArchive": { + "itemArchiveToast": { "message": "Запис архівовано" }, - "itemWasUnarchived": { - "message": "Запис розархівовано" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "Запис розархівовано" }, "archiveItem": { @@ -5964,7 +5961,7 @@ "message": "Номер картки" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Помилка: неможливо розшифрувати" }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Ваша організація більше не використовує головні паролі для входу в Bitwarden. Щоб продовжити, підтвердіть організацію та домен." @@ -6128,7 +6125,7 @@ "message": "user@bitwarden.com , user@acme.com" }, "downloadBitwardenApps": { - "message": "Download Bitwarden apps" + "message": "Завантажити програми Bitwarden" }, "emailProtected": { "message": "Е-пошту захищено" diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 0082ee1ece7..4f1165835cc 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Các mục đã lưu trữ sẽ hiển thị ở đây và sẽ bị loại khỏi kết quả tìm kiếm và gợi ý tự động điền." }, - "itemWasSentToArchive": { - "message": "Mục đã được chuyển vào kho lưu trữ" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Mục đã được bỏ lưu trữ" - }, - "itemUnarchived": { - "message": "Mục đã được bỏ lưu trữ" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Lưu trữ mục" diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 860a8c09f27..c9dd30ab08e 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "已归档的项目将显示在此处,并将被排除在一般搜索结果和自动填充建议之外。" }, - "itemWasSentToArchive": { - "message": "项目已发送到归档" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "项目已取消归档" - }, - "itemUnarchived": { - "message": "项目已取消归档" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "归档项目" diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 3f387d935d4..8da1b2ad08f 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -228,7 +228,7 @@ "message": "複製自訂欄位名稱" }, "noMatchingLogins": { - "message": "沒有符合的登入資料" + "message": "沒有相符的登入項目" }, "noCards": { "message": "沒有付款卡" @@ -252,7 +252,7 @@ "message": "登入您的密碼庫" }, "autoFillInfo": { - "message": "沒有可以在目前瀏覽器分頁自動填入的登入資訊。" + "message": "目前瀏覽器分頁沒有可自動填入的登入項目。" }, "addLogin": { "message": "新增登入資料" @@ -453,10 +453,10 @@ "description": "Short for 'credential generator'." }, "passGenInfo": { - "message": "為您的登入資料自動產生高強度且唯一的密碼。" + "message": "為您的登入項目自動產生高強度且唯一的密碼。" }, "bitWebVaultApp": { - "message": "Bitwarden 網頁應用程式" + "message": "Bitwarden Web 應用程式" }, "select": { "message": "選擇" @@ -468,16 +468,16 @@ "message": "產生密碼短語" }, "passwordGenerated": { - "message": "已產生密碼" + "message": "密碼已產生" }, "passphraseGenerated": { - "message": "已產生密碼" + "message": "密碼短語已產生" }, "usernameGenerated": { - "message": "已產生使用者名稱" + "message": "使用者名稱已產生" }, "emailGenerated": { - "message": "已產生電子郵件" + "message": "電子郵件已產生" }, "regeneratePassword": { "message": "重新產生密碼" @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "封存的項目會顯示在此處,且不會出現在一般搜尋結果或自動填入建議中。" }, - "itemWasSentToArchive": { - "message": "項目已移至封存" + "itemArchiveToast": { + "message": "項目已封存" }, - "itemWasUnarchived": { - "message": "已取消封存項目" - }, - "itemUnarchived": { - "message": "項目取消封存" + "itemUnarchivedToast": { + "message": "項目已取消封存" }, "archiveItem": { "message": "封存項目" @@ -598,7 +595,7 @@ "message": "需要進階版會員才能使用封存功能。" }, "itemRestored": { - "message": "已還原項目" + "message": "項目已還原" }, "edit": { "message": "編輯" @@ -1215,7 +1212,7 @@ "description": "Button text for saving login details as a new entry." }, "updateLoginAction": { - "message": "更新登入資料", + "message": "更新登入項目", "description": "Button text for updating an existing login entry." }, "unlockToSave": { @@ -1223,7 +1220,7 @@ "description": "User prompt to take action in order to save the login they just entered." }, "saveLogin": { - "message": "儲存登入資料", + "message": "儲存登入項目", "description": "Prompt asking the user if they want to save their login details." }, "updateLogin": { @@ -1231,7 +1228,7 @@ "description": "Prompt asking the user if they want to update an existing login entry." }, "loginSaveSuccess": { - "message": "登入資訊已儲存", + "message": "登入項目已儲存", "description": "Message displayed when login details are successfully saved." }, "loginUpdateSuccess": { @@ -1305,7 +1302,7 @@ "message": "解鎖" }, "additionalOptions": { - "message": "額外選項" + "message": "其他選項" }, "enableContextMenuItem": { "message": "顯示內容選單選項" @@ -1567,7 +1564,7 @@ "message": "提供密碼健全性、帳戶健康狀態及資料外洩報告,確保您的密碼庫安全。" }, "ppremiumSignUpTotp": { - "message": "為密碼庫中的登入資料產生 TOTP 驗證碼(2FA)。" + "message": "為密碼庫中的登入項目產生 TOTP 驗證碼(2FA)。" }, "ppremiumSignUpSupport": { "message": "優先客戶支援。" @@ -1801,7 +1798,7 @@ "message": "Bitwarden 如何保護您的資料免於網路釣魚攻擊?" }, "currentWebsite": { - "message": "目網站" + "message": "目前網站" }, "autofillAndAddWebsite": { "message": "自動填充並新增此網站" @@ -2091,7 +2088,7 @@ "message": "登入資料" }, "typeLogins": { - "message": "登入資料" + "message": "登入項目" }, "typeSecureNote": { "message": "安全筆記" @@ -2227,7 +2224,7 @@ "message": "身分" }, "logins": { - "message": "登入資料" + "message": "登入項目" }, "secureNotes": { "message": "安全筆記" @@ -2513,7 +2510,7 @@ "message": "項目已自動填入並且已儲存統一資源標識符(URI)" }, "autoFillSuccess": { - "message": "項目已自動填入 " + "message": "項目已自動填入" }, "insecurePageWarning": { "message": "警告:此為不安全的 HTTP 頁面,您送出的任何資訊都可能被他人查看並修改。此登入資料原本儲存在安全的(HTTPS)頁面上。" @@ -2773,10 +2770,10 @@ } }, "atRiskPassword": { - "message": "具有風險的密碼" + "message": "有風險的密碼" }, "atRiskPasswords": { - "message": "具有風險的密碼" + "message": "有風險的密碼" }, "atRiskPasswordDescSingleOrg": { "message": "$ORGANIZATION$ 要求你變更一組有風險的密碼。", @@ -2848,7 +2845,7 @@ "message": "更新你的設定,以便能快速自動填入密碼並產生新密碼" }, "reviewAtRiskLogins": { - "message": "檢視有風險的登入資訊" + "message": "檢視有風險的登入項目" }, "reviewAtRiskPasswords": { "message": "檢視有風險的密碼" @@ -5749,7 +5746,7 @@ "message": "設定生物辨識解鎖及自動填入,不需要輸入任何字元就可以登入。" }, "secureUser": { - "message": "升級您的登入體驗" + "message": "讓您的登入項目更升級" }, "secureUserBody": { "message": "使用密碼產生器來建立及儲存高強度、唯一的密碼,來保護您所有的帳號。" @@ -5964,7 +5961,7 @@ "message": "付款卡號碼" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "錯誤:無法解密" }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "您的組織已不再使用主密碼登入 Bitwarden。若要繼續,請驗證組織與網域。" diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 585942d7537..25c7b344982 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -14,7 +14,14 @@ import { timeout, } from "rxjs"; -import { CollectionService, DefaultCollectionService } from "@bitwarden/admin-console/common"; +import { + CollectionService, + DefaultCollectionService, + DefaultOrganizationUserApiService, + DefaultOrganizationUserService, + OrganizationUserApiService, + OrganizationUserService, +} from "@bitwarden/admin-console/common"; import { AuthRequestApiServiceAbstraction, AuthRequestService, @@ -27,6 +34,10 @@ import { LogoutReason, UserDecryptionOptionsService, } from "@bitwarden/auth/common"; +import { + AutomaticUserConfirmationService, + DefaultAutomaticUserConfirmationService, +} from "@bitwarden/auto-confirm"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; @@ -487,6 +498,9 @@ export default class MainBackground { onUpdatedRan: boolean; onReplacedRan: boolean; loginToAutoFill: CipherView = null; + organizationUserService: OrganizationUserService; + organizationUserApiService: OrganizationUserApiService; + autoConfirmService: AutomaticUserConfirmationService; private commandsBackground: CommandsBackground; private contextMenusBackground: ContextMenusBackground; @@ -763,6 +777,15 @@ export default class MainBackground { { createRequest: (url, request) => new Request(url, request) }, ); + this.organizationUserApiService = new DefaultOrganizationUserApiService(this.apiService); + this.organizationUserService = new DefaultOrganizationUserService( + this.keyService, + this.encryptService, + this.organizationUserApiService, + this.accountService, + this.i18nService, + ); + this.hibpApiService = new HibpApiService(this.apiService); this.fileUploadService = new FileUploadService(this.logService, this.apiService); this.cipherFileUploadService = new CipherFileUploadService( @@ -804,6 +827,16 @@ export default class MainBackground { this.authService, ); + this.autoConfirmService = new DefaultAutomaticUserConfirmationService( + this.configService, + this.apiService, + this.organizationUserService, + this.stateProvider, + this.organizationService, + this.organizationUserApiService, + this.policyService, + ); + const sdkClientFactory = flagEnabled("sdk") ? new DefaultSdkClientFactory() : new NoopSdkClientFactory(); @@ -1219,6 +1252,7 @@ export default class MainBackground { this.authRequestAnsweringService, this.configService, this.policyService, + this.autoConfirmService, ); this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.authService); diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.html b/apps/browser/src/platform/popup/layout/popup-page.component.html index bb24fb800aa..dc07d025e60 100644 --- a/apps/browser/src/platform/popup/layout/popup-page.component.html +++ b/apps/browser/src/platform/popup/layout/popup-page.component.html @@ -40,7 +40,7 @@ class="tw-absolute tw-inset-0 tw-flex tw-items-center tw-justify-center tw-text-main" [ngClass]="{ 'tw-invisible': !loading() }" > - + diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.ts b/apps/browser/src/platform/popup/layout/popup-page.component.ts index 7d4b7decb7f..e661bf2ca00 100644 --- a/apps/browser/src/platform/popup/layout/popup-page.component.ts +++ b/apps/browser/src/platform/popup/layout/popup-page.component.ts @@ -13,7 +13,7 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { filter, switchMap, fromEvent, startWith, map } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { ScrollLayoutHostDirective, ScrollLayoutService } from "@bitwarden/components"; +import { IconModule, ScrollLayoutHostDirective, ScrollLayoutService } from "@bitwarden/components"; @Component({ selector: "popup-page", @@ -21,7 +21,7 @@ import { ScrollLayoutHostDirective, ScrollLayoutService } from "@bitwarden/compo host: { class: "tw-h-full tw-flex tw-flex-col tw-overflow-y-hidden", }, - imports: [CommonModule, ScrollLayoutHostDirective], + imports: [CommonModule, IconModule, ScrollLayoutHostDirective], changeDetection: ChangeDetectionStrategy.OnPush, }) export class PopupPageComponent { diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 4e14d1171fd..0d85743bba7 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -42,7 +42,7 @@ import { TwoFactorAuthComponent, TwoFactorAuthGuard, } from "@bitwarden/auth/angular"; -import { canAccessAutoConfirmSettings } from "@bitwarden/auto-confirm"; +import { canAccessAutoConfirmSettings } from "@bitwarden/auto-confirm/angular"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components"; import { LockComponent, diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html index 94c1df46eea..38ef7a4f1df 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html @@ -20,7 +20,13 @@ {{ "createdSendSuccessfully" | i18n }}

- {{ formatExpirationDate() }} + @let translationKey = + send.authType === AuthType.Email + ? "sendCreatedDescriptionEmail" + : send.authType === AuthType.Password + ? "sendCreatedDescriptionPassword" + : "sendCreatedDescriptionV2"; + {{ translationKey | i18n: formattedExpirationTime }}

@@ -147,16 +141,10 @@ (click)="showAccountPreferences = !showAccountPreferences" [attr.aria-expanded]="showAccountPreferences" > - - + {{ "accountPreferences" | i18n }} @@ -222,16 +210,10 @@ (click)="showAppPreferences = !showAppPreferences" [attr.aria-expanded]="showAppPreferences" > - - + {{ "appPreferences" | i18n }} diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index f2e828b95ce..7bab7db3c29 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -45,6 +45,7 @@ import { DialogService, FormFieldModule, IconButtonModule, + IconModule, ItemModule, LinkModule, SectionComponent, @@ -89,6 +90,7 @@ import { NativeMessagingManifestService } from "../services/native-messaging-man FormsModule, ReactiveFormsModule, IconButtonModule, + IconModule, ItemModule, JslibModule, LinkModule, diff --git a/apps/desktop/src/app/layout/account-switcher.component.html b/apps/desktop/src/app/layout/account-switcher.component.html index ef177ea1bb6..7d0ee8fac83 100644 --- a/apps/desktop/src/app/layout/account-switcher.component.html +++ b/apps/desktop/src/app/layout/account-switcher.component.html @@ -31,11 +31,7 @@ {{ "switchAccount" | i18n }} - + ) - + class="bwi-2x text-muted" + >
diff --git a/apps/desktop/src/app/layout/search/search.component.html b/apps/desktop/src/app/layout/search/search.component.html index 515385c2076..b5bcd264897 100644 --- a/apps/desktop/src/app/layout/search/search.component.html +++ b/apps/desktop/src/app/layout/search/search.component.html @@ -7,5 +7,5 @@ [formControl]="searchText" appAutofocus /> - + diff --git a/apps/desktop/src/app/shared/shared.module.ts b/apps/desktop/src/app/shared/shared.module.ts index 6eed4a197f3..85b3b800e83 100644 --- a/apps/desktop/src/app/shared/shared.module.ts +++ b/apps/desktop/src/app/shared/shared.module.ts @@ -7,6 +7,7 @@ import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { IconModule } from "@bitwarden/components"; import { AvatarComponent } from "../components/avatar.component"; import { ServicesModule } from "../services/services.module"; @@ -17,6 +18,7 @@ import { ServicesModule } from "../services/services.module"; A11yModule, DragDropModule, FormsModule, + IconModule, JslibModule, OverlayModule, ReactiveFormsModule, @@ -30,6 +32,7 @@ import { ServicesModule } from "../services/services.module"; DatePipe, DragDropModule, FormsModule, + IconModule, JslibModule, OverlayModule, ReactiveFormsModule, diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index e5cd85aa7a3..cca0097d65e 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -16,7 +16,6 @@ import { import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service"; import { DeviceType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; @@ -31,7 +30,6 @@ import { import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { parseCredentialId } from "@bitwarden/common/platform/services/fido2/credential-id-utils"; import { getCredentialsForAutofill } from "@bitwarden/common/platform/services/fido2/fido2-autofill-utils"; import { Fido2Utils } from "@bitwarden/common/platform/services/fido2/fido2-utils"; import { UserId } from "@bitwarden/common/types/guid"; @@ -152,11 +150,13 @@ export class DesktopAutofillService implements OnDestroy { passwordCredentials = cipherViews .filter( (cipher) => + !cipher.isDeleted && cipher.type === CipherType.Login && cipher.login.uris?.length > 0 && cipher.login.uris.some((uri) => uri.match !== UriMatchStrategy.Never) && cipher.login.uris.some((uri) => !Utils.isNullOrWhitespace(uri.uri)) && - !Utils.isNullOrWhitespace(cipher.login.username), + !Utils.isNullOrWhitespace(cipher.login.username) && + !Utils.isNullOrWhitespace(cipher.login.password), ) .map((cipher) => ({ type: "password", @@ -258,39 +258,6 @@ export class DesktopAutofillService implements OnDestroy { const controller = new AbortController(); try { - // For some reason the credentialId is passed as an empty array in the request, so we need to - // get it from the cipher. For that we use the recordIdentifier, which is the cipherId. - if (request.recordIdentifier && request.credentialId.length === 0) { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(getOptionalUserId), - ); - if (!activeUserId) { - this.logService.error("listenPasskeyAssertion error", "Active user not found"); - callback(new Error("Active user not found"), null); - return; - } - - const cipher = await this.cipherService.get(request.recordIdentifier, activeUserId); - if (!cipher) { - this.logService.error("listenPasskeyAssertion error", "Cipher not found"); - callback(new Error("Cipher not found"), null); - return; - } - - const decrypted = await this.cipherService.decrypt(cipher, activeUserId); - - const fido2Credential = decrypted.login.fido2Credentials?.[0]; - if (!fido2Credential) { - this.logService.error("listenPasskeyAssertion error", "Fido2Credential not found"); - callback(new Error("Fido2Credential not found"), null); - return; - } - - request.credentialId = Array.from( - new Uint8Array(parseCredentialId(decrypted.login.fido2Credentials?.[0].credentialId)), - ); - } - const response = await this.fido2AuthenticatorService.getAssertion( this.convertAssertionRequest(request, true), { windowXy: normalizePosition(request.windowXy) }, diff --git a/apps/desktop/src/index.html b/apps/desktop/src/index.html index 37eb64adf35..044d7eb0e2f 100644 --- a/apps/desktop/src/index.html +++ b/apps/desktop/src/index.html @@ -13,6 +13,7 @@ +
diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index ef221f96878..c0824c61d03 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index bcac0529a8c..3e668c327b0 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index f94ff2417cf..4e5d414eb1c 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -4387,10 +4387,10 @@ "noItemsInArchiveDesc": { "message": "Arxivlənmiş elementlər burada görünəcək, ümumi axtarış nəticələrindən və avto-doldurma təkliflərindən xaric ediləcək." }, - "itemWasSentToArchive": { - "message": "Element arxivə göndərildi" + "itemArchiveToast": { + "message": "Element arxivləndi" }, - "itemWasUnarchived": { + "itemUnarchivedToast": { "message": "Element arxivdən çıxarıldı" }, "archiveItem": { @@ -4487,7 +4487,7 @@ "message": "Vaxt bitmə əməliyyatı" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Xəta: Şifrəsi açıla bilmir" }, "sessionTimeoutHeader": { "message": "Sessiya vaxt bitməsi" diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index 3467fe20ae8..f2f9d0a736d 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index cab21191e37..ea0355ad7f6 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Архивираните елементи ще се показват тук и ще бъдат изключени от общите резултати при търсене и от предложенията за автоматично попълване." }, - "itemWasSentToArchive": { - "message": "Елементът беше преместен в архива" + "itemArchiveToast": { + "message": "Елементът е преместен в архива" }, - "itemWasUnarchived": { - "message": "Елементът беше изваден от архива" + "itemUnarchivedToast": { + "message": "Елементът е изваден от архива" }, "archiveItem": { "message": "Архивиране на елемента" diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 544d88a72a6..6a211c93052 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 289554a237f..4ca3aa8ffc2 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 7b8d32a798c..3b8562814fd 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 478343b7e7d..75136c41831 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -4387,10 +4387,10 @@ "noItemsInArchiveDesc": { "message": "Zde se zobrazí archivované položky a budou vyloučeny z obecných výsledků vyhledávání a návrhů automatického vyplňování." }, - "itemWasSentToArchive": { - "message": "Položka byla přesunuta do archivu" + "itemArchiveToast": { + "message": "Položka archivována" }, - "itemWasUnarchived": { + "itemUnarchivedToast": { "message": "Položka byla odebrána z archivu" }, "archiveItem": { diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 4feb0181431..46df0aca8c5 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index bd6a6f4379a..f6abcd51740 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 205c8e95435..a2c346896ac 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archivierte Einträge werden hier angezeigt und von allgemeinen Suchergebnissen sowie Auto-Ausfüllen-Vorschlägen ausgeschlossen." }, - "itemWasSentToArchive": { - "message": "Eintrag wurde archiviert" + "itemArchiveToast": { + "message": "Eintrag archiviert" }, - "itemWasUnarchived": { - "message": "Eintrag wird nicht mehr archiviert" + "itemUnarchivedToast": { + "message": "Eintrag nicht mehr archiviert" }, "archiveItem": { "message": "Eintrag archivieren" @@ -4487,7 +4487,7 @@ "message": "Timeout-Aktion" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Fehler: Entschlüsselung nicht möglich" }, "sessionTimeoutHeader": { "message": "Sitzungs-Timeout" diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 97371668dca..624560f5888 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 85742db94ab..f444265877d 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 22b482ed04d..aaf1e12955c 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 42a63ff0db1..1dca7070bfc 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index ae37e15d84c..cefd462e99f 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index d6210f940b5..d9fb17907aa 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -773,7 +773,7 @@ "message": "Añadir adjunto" }, "itemsTransferred": { - "message": "Items transferred" + "message": "Elementos transferidos" }, "fixEncryption": { "message": "Corregir cifrado" @@ -2089,7 +2089,7 @@ "message": "Elemento eliminado de forma permanente" }, "archivedItemRestored": { - "message": "Archived item restored" + "message": "Elemento archivado restaurado" }, "restoredItem": { "message": "Elemento restaurado" @@ -4009,7 +4009,7 @@ "message": "No, no lo tengo" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "Sí, puedo acceder a mi correo electrónico de forma fiable" }, "turnOnTwoStepLogin": { "message": "Turn on two-step login" @@ -4075,10 +4075,10 @@ "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, "vulnerablePassword": { - "message": "Vulnerable password." + "message": "Contraseña vulnerable." }, "changeNow": { - "message": "Change now" + "message": "Cambiar ahora" }, "missingWebsite": { "message": "Missing website" @@ -4109,7 +4109,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsTitleNoSearchResults": { - "message": "No search results returned" + "message": "Ningún resultado de búsqueda devuelto" }, "sendsBodyNoItems": { "message": "Comparte archivos y datos de forma segura con cualquiera, en cualquier plataforma. Tu información permanecerá encriptada de extremo a extremo, limitando su exposición.", @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Los elementos archivados aparecerán aquí y se excluirán de los resultados de búsqueda generales y de sugerencias de autocompletado." }, - "itemWasSentToArchive": { - "message": "El elemento fue archivado" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "El elemento fue desarchivado" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archivar elemento" @@ -4403,7 +4403,7 @@ "message": "Desarchivar y guardar" }, "restartPremium": { - "message": "Restart Premium" + "message": "Reiniciar Premium" }, "premiumSubscriptionEnded": { "message": "Tu suscripción Premium ha terminado" @@ -4537,10 +4537,10 @@ "message": "Set an unlock method to change your timeout action" }, "upgrade": { - "message": "Upgrade" + "message": "Actualizar" }, "leaveConfirmationDialogTitle": { - "message": "Are you sure you want to leave?" + "message": "¿Estás seguro de que quieres salir?" }, "leaveConfirmationDialogContentOne": { "message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features." @@ -4558,10 +4558,10 @@ } }, "howToManageMyVault": { - "message": "How do I manage my vault?" + "message": "¿Cómo gestiono mi caja fuerte?" }, "transferItemsToOrganizationTitle": { - "message": "Transfer items to $ORGANIZATION$", + "message": "Transferir elementos a $ORGANIZATION$", "placeholders": { "organization": { "content": "$1", @@ -4579,13 +4579,13 @@ } }, "acceptTransfer": { - "message": "Accept transfer" + "message": "Aceptar transferencia" }, "declineAndLeave": { - "message": "Decline and leave" + "message": "Rechazar y salir" }, "whyAmISeeingThis": { - "message": "Why am I seeing this?" + "message": "¿Por qué estoy viendo esto?" }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", @@ -4595,25 +4595,25 @@ "message": "Email protected" }, "emails": { - "message": "Emails" + "message": "Correos electrónicos" }, "noAuth": { - "message": "Anyone with the link" + "message": "Cualquiera con el enlace" }, "anyOneWithPassword": { "message": "Anyone with a password set by you" }, "whoCanView": { - "message": "Who can view" + "message": "Quién puede ver" }, "specificPeople": { - "message": "Specific people" + "message": "Personas específicas" }, "emailVerificationDesc": { "message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send." }, "enterMultipleEmailsSeparatedByComma": { - "message": "Enter multiple emails by separating with a comma." + "message": "Introduce varios correos electrónicos separándolos con una coma." }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 8c9476cc69e..ba930db8961 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index 75c33286fb7..e03da9ef685 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index 4136edbde06..a443cc8c2e7 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "آیتم‌های بایگانی‌شده در اینجا نمایش داده می‌شوند و از نتایج جستجوی عمومی و پیشنهاد ها پر کردن خودکار حذف خواهند شد." }, - "itemWasSentToArchive": { - "message": "آیتم به بایگانی فرستاده شد" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "آیتم از بایگانی خارج شد" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "بایگانی آیتم" diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index b04b741bd3b..c7b51def9b2 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index c503efc39f9..5835821f526 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index f04807aaeb9..e8d07e28d2d 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Les éléments archivés apparaîtront ici et seront exclus des résultats de recherche généraux et des suggestions de remplissage automatique." }, - "itemWasSentToArchive": { - "message": "L'élément a été envoyé à l'archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "L'élément a été désarchivé" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archiver l'élément" diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 76904276732..6d0922bd680 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index dbb2533e03e..763401ac6fe 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "פריטים בארכיון יופיעו כאן ויוחרגו מתוצאות חיפוש כללי והצעות למילוי אוטומטי." }, - "itemWasSentToArchive": { - "message": "הפריט נשלח לארכיון" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "הפריט הוסר מהארכיון" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "העבר פריט לארכיון" diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 1bfc0674ffe..33b69ac1519 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 5ef663ab52b..2dc081fa3c7 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Arhivirane stavke biti će prikazane ovdje i biti će izuzete iz rezultata općih pretraga i preporuka auto-ispune." }, - "itemWasSentToArchive": { - "message": "Stavka poslana u arhivu" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Stavka vraćena iz arhive" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arhiviraj stavku" diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 5440b53e93d..3ec097c2a7a 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Az archivált elemek itt jelennek meg és kizárásra kerülnek az általános keresési eredményekből és az automatikus kitöltési javaslatokból." }, - "itemWasSentToArchive": { - "message": "Az elem az archivumba került." + "itemArchiveToast": { + "message": "Az elem archiválásra került." }, - "itemWasUnarchived": { - "message": "Az elem visszavéelre került az archivumból." + "itemUnarchivedToast": { + "message": "Az elem visszavételre került az archivumból." }, "archiveItem": { "message": "Elem archiválása" diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index f4de0deff33..7648f4bb99b 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index 9cd4783407d..eb2ade245a0 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Gli elementi archiviati appariranno qui e saranno esclusi dai risultati di ricerca e dal riempimento automatico." }, - "itemWasSentToArchive": { - "message": "Elemento archiviato" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Elemento estratto dall'archivio" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archivia elemento" diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index 1d46532a980..a9b05f728d8 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index 4618fd024a9..68bba7fcb27 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 76904276732..6d0922bd680 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 3bb7f513701..3c6aced3a73 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 70ffd234941..8932f0efb48 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index f703b1f5d53..a19856a776e 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index e82a4b91487..8a863256ed1 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Šeit parādīsies arhivētie vienumi, un tie netiks iekļauti vispārējās meklēšanas iznākumos un automātiskās aizpildes ieteikumos." }, - "itemWasSentToArchive": { - "message": "Vienums tika ievietots arhīvā" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Vienums tika izņemts no arhīva" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arhivēt vienumu" diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index 25bb0cbc816..773a596a10d 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 43e0dc85fb0..3bddc3baa5b 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 76904276732..6d0922bd680 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index ddc8bef0241..22f4a30329a 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index 792c95eb1ec..b2b1631fe04 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 89c3d3ba231..b9eba55d8bd 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index e4b1d6d8abc..306bf31efe2 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Gearchiveerde items verschijnen hier en worden uitgesloten van algemene zoekresultaten en automatisch invulsuggesties." }, - "itemWasSentToArchive": { - "message": "Item naar archief verzonden" + "itemArchiveToast": { + "message": "Item gearchiveerd" }, - "itemWasUnarchived": { - "message": "Item uit het archief gehaald" + "itemUnarchivedToast": { + "message": "Item gedearchiveerd" }, "archiveItem": { "message": "Item archiveren" diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index 850696f8fcf..dc62d73a236 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index b63a970e9c2..e8ea506a873 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index e2d9fbce4a2..0cee8d6683d 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Zarchiwizowane elementy pojawią się tutaj i zostaną wykluczone z wyników wyszukiwania i sugestii autouzupełniania." }, - "itemWasSentToArchive": { - "message": "Element został przeniesiony do archiwum" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Element został usunięty z archiwum" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archiwizuj element" @@ -4487,7 +4487,7 @@ "message": "Timeout action" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Błąd: Nie można odszyfrować" }, "sessionTimeoutHeader": { "message": "Session timeout" diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index 3a055bdb03d..817e9de0c50 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais de busca e das sugestões de preenchimento automático." }, - "itemWasSentToArchive": { - "message": "O item foi enviado para o arquivo" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "O item foi desarquivado" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arquivar item" diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index bb99678bde6..3076291a4a3 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais da pesquisa e das sugestões de preenchimento automático." }, - "itemWasSentToArchive": { - "message": "O item foi movido para o arquivo" + "itemArchiveToast": { + "message": "Item arquivado" }, - "itemWasUnarchived": { - "message": "O item foi desarquivado" + "itemUnarchivedToast": { + "message": "Item desarquivado" }, "archiveItem": { "message": "Arquivar item" diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 6b51cb9fecd..b8fc25b4105 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 389f4b37dfd..089e815fefe 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Архивированные элементы появятся здесь и будут исключены из общих результатов поиска и предложений автозаполнения." }, - "itemWasSentToArchive": { - "message": "Элемент был отправлен в архив" + "itemArchiveToast": { + "message": "Элемент архивирован" }, - "itemWasUnarchived": { - "message": "Элемент был разархивирован" + "itemUnarchivedToast": { + "message": "Элемент разархивирован" }, "archiveItem": { "message": "Архивировать элемент" diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 9e8a727ad5d..a1a84b8ba15 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 471984785d8..2876361bbf0 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -4387,10 +4387,10 @@ "noItemsInArchiveDesc": { "message": "Tu sa zobrazia archivované položky, ktoré budú vylúčené zo všeobecného vyhľadávania a z návrhov automatického vypĺňania." }, - "itemWasSentToArchive": { + "itemArchiveToast": { "message": "Položka bola archivovaná" }, - "itemWasUnarchived": { + "itemUnarchivedToast": { "message": "Položka bola odobraná z archívu" }, "archiveItem": { diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 23d9f18fadb..82e0a20b29e 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 1a68810bca3..633d3123242 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Архивиране ставке ће се овде појавити и бити искључени из општих резултата претраге и сугестија о ауто-пуњењу." }, - "itemWasSentToArchive": { - "message": "Ставка је послата у архиву" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Ставка враћена из архиве" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Архивирај ставку" diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index 1dd66c409c0..ed9e62e8319 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Arkiverade objekt kommer att visas här och kommer att uteslutas från allmänna sökresultat och förslag för autofyll." }, - "itemWasSentToArchive": { - "message": "Objektet skickades till arkivet" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Objektet har avarkiverats" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arkivera objekt" diff --git a/apps/desktop/src/locales/ta/messages.json b/apps/desktop/src/locales/ta/messages.json index 3a7fc795668..53e155874f6 100644 --- a/apps/desktop/src/locales/ta/messages.json +++ b/apps/desktop/src/locales/ta/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 76904276732..6d0922bd680 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index bcff8d0849e..b7117a7ccad 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 936a68516bd..96bb11c7a35 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -4387,10 +4387,10 @@ "noItemsInArchiveDesc": { "message": "Arşivlenmiş kayıtlar burada görünecek ve genel arama sonuçları ile otomatik doldurma önerilerinden hariç tutulacaktır." }, - "itemWasSentToArchive": { - "message": "Kayıt arşive gönderildi" + "itemArchiveToast": { + "message": "Kayıt arşivlendi" }, - "itemWasUnarchived": { + "itemUnarchivedToast": { "message": "Kayıt arşivden çıkarıldı" }, "archiveItem": { diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 2b4cfcb7c22..abcdbea0b1f 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Архівовані записи з'являтимуться тут і будуть виключені з результатів звичайного пошуку та пропозицій автозаповнення." }, - "itemWasSentToArchive": { - "message": "Запис архівовано" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Запис розархівовано" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Архівувати запис" diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index 56c9d9a5c6e..f06556568a1 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Các mục đã lưu trữ sẽ hiển thị ở đây và sẽ bị loại khỏi kết quả tìm kiếm và gợi ý tự động điền." }, - "itemWasSentToArchive": { - "message": "Mục đã được chuyển vào kho lưu trữ" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Mục đã được bỏ lưu trữ" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Lưu trữ mục" diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index cf42adba294..9941e296da3 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "已归档的项目将显示在此处,并将被排除在一般搜索结果和自动填充建议之外。" }, - "itemWasSentToArchive": { - "message": "项目已发送到归档" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "项目已取消归档" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "归档项目" diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 3157e929e8f..461eb031068 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "封存的項目會顯示在此處,且不會出現在一般搜尋結果或自動填入建議中。" }, - "itemWasSentToArchive": { - "message": "項目已移至封存" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "項目取消封存" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "封存項目" diff --git a/apps/web/src/app/admin-console/common/base-members.component.ts b/apps/web/src/app/admin-console/common/base-members.component.ts deleted file mode 100644 index 5ecf4269a1a..00000000000 --- a/apps/web/src/app/admin-console/common/base-members.component.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { Directive } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { FormControl } from "@angular/forms"; -import { firstValueFrom, lastValueFrom, debounceTime, combineLatest, BehaviorSubject } from "rxjs"; - -import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; -import { - OrganizationUserStatusType, - OrganizationUserType, - ProviderUserStatusType, - ProviderUserType, -} from "@bitwarden/common/admin-console/enums"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response"; -import { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { KeyService } from "@bitwarden/key-management"; - -import { OrganizationUserView } from "../organizations/core/views/organization-user.view"; -import { UserConfirmComponent } from "../organizations/manage/user-confirm.component"; -import { MemberActionResult } from "../organizations/members/services/member-actions/member-actions.service"; - -import { PeopleTableDataSource, peopleFilter } from "./people-table-data-source"; - -export type StatusType = OrganizationUserStatusType | ProviderUserStatusType; -export type UserViewTypes = ProviderUserUserDetailsResponse | OrganizationUserView; - -/** - * A refactored copy of BasePeopleComponent, using the component library table and other modern features. - * This will replace BasePeopleComponent once all subclasses have been changed over to use this class. - */ -@Directive() -export abstract class BaseMembersComponent { - /** - * Shows a banner alerting the admin that users need to be confirmed. - */ - get showConfirmUsers(): boolean { - return ( - this.dataSource.activeUserCount > 1 && - this.dataSource.confirmedUserCount > 0 && - this.dataSource.confirmedUserCount < 3 && - this.dataSource.acceptedUserCount > 0 - ); - } - - get showBulkConfirmUsers(): boolean { - return this.dataSource - .getCheckedUsers() - .every((member) => member.status == this.userStatusType.Accepted); - } - - get showBulkReinviteUsers(): boolean { - return this.dataSource - .getCheckedUsers() - .every((member) => member.status == this.userStatusType.Invited); - } - - abstract userType: typeof OrganizationUserType | typeof ProviderUserType; - abstract userStatusType: typeof OrganizationUserStatusType | typeof ProviderUserStatusType; - - protected abstract dataSource: PeopleTableDataSource; - - firstLoaded: boolean = false; - - /** - * The currently selected status filter, or undefined to show all active users. - */ - status?: StatusType; - - /** - * The currently executing promise - used to avoid multiple user actions executing at once. - */ - actionPromise?: Promise; - - protected searchControl = new FormControl("", { nonNullable: true }); - protected statusToggle = new BehaviorSubject(undefined); - - constructor( - protected apiService: ApiService, - protected i18nService: I18nService, - protected keyService: KeyService, - protected validationService: ValidationService, - protected logService: LogService, - protected userNamePipe: UserNamePipe, - protected dialogService: DialogService, - protected organizationManagementPreferencesService: OrganizationManagementPreferencesService, - protected toastService: ToastService, - ) { - // Connect the search input and status toggles to the table dataSource filter - combineLatest([this.searchControl.valueChanges.pipe(debounceTime(200)), this.statusToggle]) - .pipe(takeUntilDestroyed()) - .subscribe( - ([searchText, status]) => (this.dataSource.filter = peopleFilter(searchText, status)), - ); - } - - abstract edit(user: UserView, organization?: Organization): void; - abstract getUsers(organization?: Organization): Promise | UserView[]>; - abstract removeUser(id: string, organization?: Organization): Promise; - abstract reinviteUser(id: string, organization?: Organization): Promise; - abstract confirmUser( - user: UserView, - publicKey: Uint8Array, - organization?: Organization, - ): Promise; - abstract invite(organization?: Organization): void; - - async load(organization?: Organization) { - // Load new users from the server - const response = await this.getUsers(organization); - - // GetUsers can return a ListResponse or an Array - if (response instanceof ListResponse) { - this.dataSource.data = response.data != null && response.data.length > 0 ? response.data : []; - } else if (Array.isArray(response)) { - this.dataSource.data = response; - } - - this.firstLoaded = true; - } - - protected async removeUserConfirmationDialog(user: UserView) { - return this.dialogService.openSimpleDialog({ - title: this.userNamePipe.transform(user), - content: { key: "removeUserConfirmation" }, - type: "warning", - }); - } - - async remove(user: UserView, organization?: Organization) { - const confirmed = await this.removeUserConfirmationDialog(user); - if (!confirmed) { - return false; - } - - this.actionPromise = this.removeUser(user.id, organization); - try { - const result = await this.actionPromise; - if (result.success) { - this.toastService.showToast({ - variant: "success", - message: this.i18nService.t("removedUserId", this.userNamePipe.transform(user)), - }); - this.dataSource.removeUser(user); - } else { - throw new Error(result.error); - } - } catch (e) { - this.validationService.showError(e); - } - this.actionPromise = undefined; - } - - async reinvite(user: UserView, organization?: Organization) { - if (this.actionPromise != null) { - return; - } - - this.actionPromise = this.reinviteUser(user.id, organization); - try { - const result = await this.actionPromise; - if (result.success) { - this.toastService.showToast({ - variant: "success", - message: this.i18nService.t("hasBeenReinvited", this.userNamePipe.transform(user)), - }); - } else { - throw new Error(result.error); - } - } catch (e) { - this.validationService.showError(e); - } - this.actionPromise = undefined; - } - - async confirm(user: UserView, organization?: Organization) { - const confirmUser = async (publicKey: Uint8Array) => { - try { - this.actionPromise = this.confirmUser(user, publicKey, organization); - const result = await this.actionPromise; - if (result.success) { - user.status = this.userStatusType.Confirmed; - this.dataSource.replaceUser(user); - - this.toastService.showToast({ - variant: "success", - message: this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(user)), - }); - } else { - throw new Error(result.error); - } - } catch (e) { - this.validationService.showError(e); - throw e; - } finally { - this.actionPromise = undefined; - } - }; - - if (this.actionPromise != null) { - return; - } - - try { - const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId); - const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey); - - const autoConfirm = await firstValueFrom( - this.organizationManagementPreferencesService.autoConfirmFingerPrints.state$, - ); - if (user == null) { - throw new Error("Cannot confirm null user."); - } - if (autoConfirm == null || !autoConfirm) { - const dialogRef = UserConfirmComponent.open(this.dialogService, { - data: { - name: this.userNamePipe.transform(user), - userId: user.userId, - publicKey: publicKey, - confirmUser: () => confirmUser(publicKey), - }, - }); - await lastValueFrom(dialogRef.closed); - - return; - } - - try { - const fingerprint = await this.keyService.getFingerprint(user.userId, publicKey); - this.logService.info(`User's fingerprint: ${fingerprint.join("-")}`); - } catch (e) { - this.logService.error(e); - } - await confirmUser(publicKey); - } catch (e) { - this.logService.error(`Handled exception: ${e}`); - } - } -} diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts index 073d73f6a50..a641116f4de 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts @@ -472,7 +472,7 @@ export class VaultComponent implements OnInit, OnDestroy { collections, filter.collectionId, ); - searchableCollectionNodes = selectedCollection.children ?? []; + searchableCollectionNodes = selectedCollection?.children ?? []; } let collectionsToReturn: CollectionAdminView[] = []; @@ -962,10 +962,10 @@ export class VaultComponent implements OnInit, OnDestroy { await this.editCipher(cipher, true); } - restore = async (c: CipherViewLike): Promise => { + restore = async (c: CipherViewLike): Promise => { const organization = await firstValueFrom(this.organization$); if (!CipherViewLikeUtils.isDeleted(c)) { - return false; + return; } if ( @@ -974,11 +974,11 @@ export class VaultComponent implements OnInit, OnDestroy { !organization.allowAdminAccessToAllCollectionItems ) { this.showMissingPermissionsError(); - return false; + return; } if (!(await this.repromptCipher([c]))) { - return false; + return; } // Allow restore of an Unassigned Item @@ -996,10 +996,10 @@ export class VaultComponent implements OnInit, OnDestroy { message: this.i18nService.t("restoredItem"), }); this.refresh(); - return true; + return; } catch (e) { this.logService.error(e); - return false; + return; } }; diff --git a/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts b/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts index 03130d0b946..788d01695b0 100644 --- a/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts @@ -2,11 +2,8 @@ // @ts-strict-ignore import { Component, Inject, OnInit } from "@angular/core"; import { FormControl, FormGroup } from "@angular/forms"; -import { firstValueFrom } from "rxjs"; import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; -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 { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; @@ -17,8 +14,6 @@ export type UserConfirmDialogData = { name: string; userId: string; publicKey: Uint8Array; - // @TODO remove this when doing feature flag cleanup for members component refactor. - confirmUser?: (publicKey: Uint8Array) => Promise; }; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush @@ -46,7 +41,6 @@ export class UserConfirmComponent implements OnInit { private keyService: KeyService, private logService: LogService, private organizationManagementPreferencesService: OrganizationManagementPreferencesService, - private configService: ConfigService, ) { this.name = data.name; this.userId = data.userId; @@ -76,13 +70,6 @@ export class UserConfirmComponent implements OnInit { await this.organizationManagementPreferencesService.autoConfirmFingerPrints.set(true); } - const membersComponentRefactorEnabled = await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.MembersComponentRefactor), - ); - if (!membersComponentRefactorEnabled) { - await this.data.confirmUser(this.publicKey); - } - this.dialogRef.close(true); }; diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts index 5c9bf919ed4..cfddb17627a 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts @@ -9,7 +9,6 @@ import { } from "@bitwarden/common/admin-console/enums"; import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response"; -import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components"; @@ -34,7 +33,7 @@ type BulkStatusEntry = { type BulkStatusDialogData = { users: Array; filteredUsers: Array; - request: Promise>; + request: Promise; successfulMessage: string; }; @@ -63,7 +62,7 @@ export class BulkStatusComponent implements OnInit { async showBulkStatus(data: BulkStatusDialogData) { try { const response = await data.request; - const keyedErrors: any = response.data + const keyedErrors: any = (response ?? []) .filter((r) => r.error !== "") .reduce((a, x) => ({ ...a, [x.id]: x.error }), {}); const keyedFilteredUsers: any = data.filteredUsers.reduce( diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index 6848f76286f..43520449535 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -195,9 +195,9 @@ export class MemberDialogComponent implements OnDestroy { private accountService: AccountService, organizationService: OrganizationService, private toastService: ToastService, - private configService: ConfigService, private deleteManagedMemberWarningService: DeleteManagedMemberWarningService, private organizationUserService: OrganizationUserService, + private configService: ConfigService, ) { this.organization$ = accountService.activeAccount$.pipe( getUserId, diff --git a/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.html b/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.html deleted file mode 100644 index 65bab31c728..00000000000 --- a/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.html +++ /dev/null @@ -1,495 +0,0 @@ -@let organization = this.organization(); -@if (organization) { - - - - - - - - -
- - - {{ "all" | i18n }} - {{ - allCount - }} - - - - {{ "invited" | i18n }} - {{ - invitedCount - }} - - - - {{ "needsConfirmation" | i18n }} - {{ - acceptedUserCount - }} - - - - {{ "revoked" | i18n }} - {{ - revokedCount - }} - - -
- - - {{ "loading" | i18n }} - - -

{{ "noMembersInList" | i18n }}

- - - {{ "usersNeedConfirmed" | i18n }} - - - - - - - - - - - {{ "name" | i18n }} - {{ (organization.useGroups ? "groups" : "collections") | i18n }} - {{ "role" | i18n }} - {{ "policies" | i18n }} - -
- - -
- - - - - - - - - - - - - - - -
- - - - - - - -
- -
-
- - - {{ "invited" | i18n }} - - - {{ "needsConfirmation" | i18n }} - - - {{ "revoked" | i18n }} - -
-
- {{ u.email }} -
-
-
- -
- - -
- -
-
- {{ u.name ?? u.email }} - - {{ "invited" | i18n }} - - - {{ "needsConfirmation" | i18n }} - - - {{ "revoked" | i18n }} - -
-
- {{ u.email }} -
-
-
- -
- - - - - - - - - - - - - - - {{ u.type | userType }} - - - - - {{ u.type | userType }} - - - - - - - {{ "userUsingTwoStep" | i18n }} - - @let resetPasswordPolicyEnabled = resetPasswordPolicyEnabled$ | async; - - - {{ "enrolledAccountRecovery" | i18n }} - - - -
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
-} diff --git a/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.ts b/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.ts deleted file mode 100644 index dae9bafbcfe..00000000000 --- a/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.ts +++ /dev/null @@ -1,624 +0,0 @@ -import { Component, computed, Signal } from "@angular/core"; -import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop"; -import { ActivatedRoute } from "@angular/router"; -import { - combineLatest, - concatMap, - filter, - firstValueFrom, - from, - map, - merge, - Observable, - shareReplay, - switchMap, - take, -} from "rxjs"; - -import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common"; -import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; -import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { - OrganizationUserStatusType, - OrganizationUserType, - PolicyType, -} from "@bitwarden/common/admin-console/enums"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-metadata.service.abstraction"; -import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { getById } from "@bitwarden/common/platform/misc"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { KeyService } from "@bitwarden/key-management"; -import { UserId } from "@bitwarden/user-core"; -import { BillingConstraintService } from "@bitwarden/web-vault/app/billing/members/billing-constraint/billing-constraint.service"; -import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; - -import { BaseMembersComponent } from "../../common/base-members.component"; -import { - CloudBulkReinviteLimit, - MaxCheckedCount, - PeopleTableDataSource, -} from "../../common/people-table-data-source"; -import { OrganizationUserView } from "../core/views/organization-user.view"; - -import { AccountRecoveryDialogResultType } from "./components/account-recovery/account-recovery-dialog.component"; -import { MemberDialogResult, MemberDialogTab } from "./components/member-dialog"; -import { - MemberDialogManagerService, - MemberExportService, - OrganizationMembersService, -} from "./services"; -import { DeleteManagedMemberWarningService } from "./services/delete-managed-member/delete-managed-member-warning.service"; -import { - MemberActionsService, - MemberActionResult, -} from "./services/member-actions/member-actions.service"; - -class MembersTableDataSource extends PeopleTableDataSource { - protected statusType = OrganizationUserStatusType; -} - -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -@Component({ - templateUrl: "deprecated_members.component.html", - standalone: false, -}) -export class MembersComponent extends BaseMembersComponent { - userType = OrganizationUserType; - userStatusType = OrganizationUserStatusType; - memberTab = MemberDialogTab; - protected dataSource: MembersTableDataSource; - - readonly organization: Signal; - status: OrganizationUserStatusType | undefined; - - private userId$: Observable = this.accountService.activeAccount$.pipe(getUserId); - - resetPasswordPolicyEnabled$: Observable; - - protected readonly canUseSecretsManager: Signal = computed( - () => this.organization()?.useSecretsManager ?? false, - ); - protected readonly showUserManagementControls: Signal = computed( - () => this.organization()?.canManageUsers ?? false, - ); - protected billingMetadata$: Observable; - - // Fixed sizes used for cdkVirtualScroll - protected rowHeight = 66; - protected rowHeightClass = `tw-h-[66px]`; - - constructor( - apiService: ApiService, - i18nService: I18nService, - organizationManagementPreferencesService: OrganizationManagementPreferencesService, - keyService: KeyService, - validationService: ValidationService, - logService: LogService, - userNamePipe: UserNamePipe, - dialogService: DialogService, - toastService: ToastService, - private route: ActivatedRoute, - protected deleteManagedMemberWarningService: DeleteManagedMemberWarningService, - private organizationWarningsService: OrganizationWarningsService, - private memberActionsService: MemberActionsService, - private memberDialogManager: MemberDialogManagerService, - protected billingConstraint: BillingConstraintService, - protected memberService: OrganizationMembersService, - private organizationService: OrganizationService, - private accountService: AccountService, - private policyService: PolicyService, - private policyApiService: PolicyApiServiceAbstraction, - private organizationMetadataService: OrganizationMetadataServiceAbstraction, - private memberExportService: MemberExportService, - private environmentService: EnvironmentService, - ) { - super( - apiService, - i18nService, - keyService, - validationService, - logService, - userNamePipe, - dialogService, - organizationManagementPreferencesService, - toastService, - ); - - this.dataSource = new MembersTableDataSource(this.environmentService); - - const organization$ = this.route.params.pipe( - concatMap((params) => - this.userId$.pipe( - switchMap((userId) => - this.organizationService.organizations$(userId).pipe(getById(params.organizationId)), - ), - filter((organization): organization is Organization => organization != null), - shareReplay({ refCount: true, bufferSize: 1 }), - ), - ), - ); - - this.organization = toSignal(organization$); - - const policies$ = combineLatest([this.userId$, organization$]).pipe( - switchMap(([userId, organization]) => - organization.isProviderUser - ? from(this.policyApiService.getPolicies(organization.id)).pipe( - map((response) => Policy.fromListResponse(response)), - ) - : this.policyService.policies$(userId), - ), - ); - - this.resetPasswordPolicyEnabled$ = combineLatest([organization$, policies$]).pipe( - map( - ([organization, policies]) => - policies - .filter((policy) => policy.type === PolicyType.ResetPassword) - .find((p) => p.organizationId === organization.id)?.enabled ?? false, - ), - ); - - combineLatest([this.route.queryParams, organization$]) - .pipe( - concatMap(async ([qParams, organization]) => { - await this.load(organization!); - - this.searchControl.setValue(qParams.search); - - if (qParams.viewEvents != null) { - const user = this.dataSource.data.filter((u) => u.id === qParams.viewEvents); - if (user.length > 0 && user[0].status === OrganizationUserStatusType.Confirmed) { - this.openEventsDialog(user[0], organization!); - } - } - }), - takeUntilDestroyed(), - ) - .subscribe(); - - organization$ - .pipe( - switchMap((organization) => - merge( - this.organizationWarningsService.showInactiveSubscriptionDialog$(organization), - this.organizationWarningsService.showSubscribeBeforeFreeTrialEndsDialog$(organization), - ), - ), - takeUntilDestroyed(), - ) - .subscribe(); - - this.billingMetadata$ = organization$.pipe( - switchMap((organization) => - this.organizationMetadataService.getOrganizationMetadata$(organization.id), - ), - shareReplay({ bufferSize: 1, refCount: false }), - ); - - // Stripe is slow, so kick this off in the background but without blocking page load. - // Anyone who needs it will still await the first emission. - this.billingMetadata$.pipe(take(1), takeUntilDestroyed()).subscribe(); - } - - override async load(organization: Organization) { - await super.load(organization); - } - - async getUsers(organization: Organization): Promise { - return await this.memberService.loadUsers(organization); - } - - async removeUser(id: string, organization: Organization): Promise { - return await this.memberActionsService.removeUser(organization, id); - } - - async revokeUser(id: string, organization: Organization): Promise { - return await this.memberActionsService.revokeUser(organization, id); - } - - async restoreUser(id: string, organization: Organization): Promise { - return await this.memberActionsService.restoreUser(organization, id); - } - - async reinviteUser(id: string, organization: Organization): Promise { - return await this.memberActionsService.reinviteUser(organization, id); - } - - async confirmUser( - user: OrganizationUserView, - publicKey: Uint8Array, - organization: Organization, - ): Promise { - return await this.memberActionsService.confirmUser(user, publicKey, organization); - } - - async revoke(user: OrganizationUserView, organization: Organization) { - const confirmed = await this.revokeUserConfirmationDialog(user); - - if (!confirmed) { - return false; - } - - this.actionPromise = this.revokeUser(user.id, organization); - try { - const result = await this.actionPromise; - if (result.success) { - this.toastService.showToast({ - variant: "success", - message: this.i18nService.t("revokedUserId", this.userNamePipe.transform(user)), - }); - await this.load(organization); - } else { - throw new Error(result.error); - } - } catch (e) { - this.validationService.showError(e); - } - this.actionPromise = undefined; - } - - async restore(user: OrganizationUserView, organization: Organization) { - this.actionPromise = this.restoreUser(user.id, organization); - try { - const result = await this.actionPromise; - if (result.success) { - this.toastService.showToast({ - variant: "success", - message: this.i18nService.t("restoredUserId", this.userNamePipe.transform(user)), - }); - await this.load(organization); - } else { - throw new Error(result.error); - } - } catch (e) { - this.validationService.showError(e); - } - this.actionPromise = undefined; - } - - allowResetPassword( - orgUser: OrganizationUserView, - organization: Organization, - orgResetPasswordPolicyEnabled: boolean, - ): boolean { - return this.memberActionsService.allowResetPassword( - orgUser, - organization, - orgResetPasswordPolicyEnabled, - ); - } - - showEnrolledStatus( - orgUser: OrganizationUserUserDetailsResponse, - organization: Organization, - orgResetPasswordPolicyEnabled: boolean, - ): boolean { - return ( - organization.useResetPassword && - orgUser.resetPasswordEnrolled && - orgResetPasswordPolicyEnabled - ); - } - - private async handleInviteDialog(organization: Organization) { - const billingMetadata = await firstValueFrom(this.billingMetadata$); - const allUserEmails = this.dataSource.data?.map((user) => user.email) ?? []; - - const result = await this.memberDialogManager.openInviteDialog( - organization, - billingMetadata, - allUserEmails, - ); - - if (result === MemberDialogResult.Saved) { - await this.load(organization); - } - } - - async invite(organization: Organization) { - const billingMetadata = await firstValueFrom(this.billingMetadata$); - const seatLimitResult = this.billingConstraint.checkSeatLimit(organization, billingMetadata); - if (!(await this.billingConstraint.seatLimitReached(seatLimitResult, organization))) { - await this.handleInviteDialog(organization); - this.organizationMetadataService.refreshMetadataCache(); - } - } - - async edit( - user: OrganizationUserView, - organization: Organization, - initialTab: MemberDialogTab = MemberDialogTab.Role, - ) { - const billingMetadata = await firstValueFrom(this.billingMetadata$); - - const result = await this.memberDialogManager.openEditDialog( - user, - organization, - billingMetadata, - initialTab, - ); - - switch (result) { - case MemberDialogResult.Deleted: - this.dataSource.removeUser(user); - break; - case MemberDialogResult.Saved: - case MemberDialogResult.Revoked: - case MemberDialogResult.Restored: - await this.load(organization); - break; - } - } - - async bulkRemove(organization: Organization) { - if (this.actionPromise != null) { - return; - } - - const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount); - - await this.memberDialogManager.openBulkRemoveDialog(organization, users); - this.organizationMetadataService.refreshMetadataCache(); - await this.load(organization); - } - - async bulkDelete(organization: Organization) { - if (this.actionPromise != null) { - return; - } - - const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount); - - await this.memberDialogManager.openBulkDeleteDialog(organization, users); - await this.load(organization); - } - - async bulkRevoke(organization: Organization) { - await this.bulkRevokeOrRestore(true, organization); - } - - async bulkRestore(organization: Organization) { - await this.bulkRevokeOrRestore(false, organization); - } - - async bulkRevokeOrRestore(isRevoking: boolean, organization: Organization) { - if (this.actionPromise != null) { - return; - } - - const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount); - - await this.memberDialogManager.openBulkRestoreRevokeDialog(organization, users, isRevoking); - await this.load(organization); - } - - async bulkReinvite(organization: Organization) { - if (this.actionPromise != null) { - return; - } - - let users: OrganizationUserView[]; - if (this.dataSource.isIncreasedBulkLimitEnabled()) { - users = this.dataSource.getCheckedUsersInVisibleOrder(); - } else { - users = this.dataSource.getCheckedUsers(); - } - - const allInvitedUsers = users.filter((u) => u.status === OrganizationUserStatusType.Invited); - - // Capture the original count BEFORE enforcing the limit - const originalInvitedCount = allInvitedUsers.length; - - // When feature flag is enabled, limit invited users and uncheck the excess - let filteredUsers: OrganizationUserView[]; - if (this.dataSource.isIncreasedBulkLimitEnabled()) { - filteredUsers = this.dataSource.limitAndUncheckExcess( - allInvitedUsers, - CloudBulkReinviteLimit, - ); - } else { - filteredUsers = allInvitedUsers; - } - - if (filteredUsers.length <= 0) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("noSelectedUsersApplicable"), - }); - return; - } - - try { - const result = await this.memberActionsService.bulkReinvite(organization, filteredUsers); - - if (!result.successful) { - throw new Error(); - } - - // When feature flag is enabled, show toast instead of dialog - if (this.dataSource.isIncreasedBulkLimitEnabled()) { - const selectedCount = originalInvitedCount; - const invitedCount = filteredUsers.length; - - if (selectedCount > CloudBulkReinviteLimit) { - const excludedCount = selectedCount - CloudBulkReinviteLimit; - this.toastService.showToast({ - variant: "success", - message: this.i18nService.t( - "bulkReinviteLimitedSuccessToast", - CloudBulkReinviteLimit.toLocaleString(), - selectedCount.toLocaleString(), - excludedCount.toLocaleString(), - ), - }); - } else { - this.toastService.showToast({ - variant: "success", - message: - invitedCount === 1 - ? this.i18nService.t("reinviteSuccessToast") - : this.i18nService.t("bulkReinviteSentToast", invitedCount.toString()), - }); - } - } else { - // Feature flag disabled - show legacy dialog - await this.memberDialogManager.openBulkStatusDialog( - users, - filteredUsers, - Promise.resolve(result.successful), - this.i18nService.t("bulkReinviteMessage"), - ); - } - } catch (e) { - this.validationService.showError(e); - } - this.actionPromise = undefined; - } - - async bulkConfirm(organization: Organization) { - if (this.actionPromise != null) { - return; - } - - const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount); - - await this.memberDialogManager.openBulkConfirmDialog(organization, users); - await this.load(organization); - } - - async bulkEnableSM(organization: Organization) { - const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount); - - await this.memberDialogManager.openBulkEnableSecretsManagerDialog(organization, users); - - this.dataSource.uncheckAllUsers(); - await this.load(organization); - } - - openEventsDialog(user: OrganizationUserView, organization: Organization) { - this.memberDialogManager.openEventsDialog(user, organization); - } - - async resetPassword(user: OrganizationUserView, organization: Organization) { - if (!user || !user.email || !user.id) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("orgUserDetailsNotFound"), - }); - this.logService.error("Org user details not found when attempting account recovery"); - - return; - } - - const result = await this.memberDialogManager.openAccountRecoveryDialog(user, organization); - if (result === AccountRecoveryDialogResultType.Ok) { - await this.load(organization); - } - - return; - } - - protected async removeUserConfirmationDialog(user: OrganizationUserView) { - return await this.memberDialogManager.openRemoveUserConfirmationDialog(user); - } - - protected async revokeUserConfirmationDialog(user: OrganizationUserView) { - return await this.memberDialogManager.openRevokeUserConfirmationDialog(user); - } - - async deleteUser(user: OrganizationUserView, organization: Organization) { - const confirmed = await this.memberDialogManager.openDeleteUserConfirmationDialog( - user, - organization, - ); - - if (!confirmed) { - return false; - } - - this.actionPromise = this.memberActionsService.deleteUser(organization, user.id); - try { - const result = await this.actionPromise; - if (!result.success) { - throw new Error(result.error); - } - this.toastService.showToast({ - variant: "success", - message: this.i18nService.t("organizationUserDeleted", this.userNamePipe.transform(user)), - }); - this.dataSource.removeUser(user); - } catch (e) { - this.validationService.showError(e); - } - this.actionPromise = undefined; - } - - get showBulkRestoreUsers(): boolean { - return this.dataSource - .getCheckedUsers() - .every((member) => member.status == this.userStatusType.Revoked); - } - - get showBulkRevokeUsers(): boolean { - return this.dataSource - .getCheckedUsers() - .every((member) => member.status != this.userStatusType.Revoked); - } - - get showBulkRemoveUsers(): boolean { - return this.dataSource.getCheckedUsers().every((member) => !member.managedByOrganization); - } - - get showBulkDeleteUsers(): boolean { - const validStatuses = [ - this.userStatusType.Accepted, - this.userStatusType.Confirmed, - this.userStatusType.Revoked, - ]; - - return this.dataSource - .getCheckedUsers() - .every((member) => member.managedByOrganization && validStatuses.includes(member.status)); - } - - get selectedInvitedCount(): number { - return this.dataSource - .getCheckedUsers() - .filter((member) => member.status === this.userStatusType.Invited).length; - } - - get isSingleInvite(): boolean { - return this.selectedInvitedCount === 1; - } - - exportMembers = () => { - const result = this.memberExportService.getMemberExport(this.dataSource.data); - if (result.success) { - this.toastService.showToast({ - variant: "success", - title: undefined, - message: this.i18nService.t("dataExportSuccess"), - }); - } - - if (result.error != null) { - this.validationService.showError(result.error.message); - } - }; -} diff --git a/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts b/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts index 2f22b9871b7..153a2f3a956 100644 --- a/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts @@ -1,30 +1,23 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; -import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; import { canAccessMembersTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FreeBitwardenFamiliesComponent } from "../../../billing/members/free-bitwarden-families.component"; import { organizationPermissionsGuard } from "../guards/org-permissions.guard"; import { canAccessSponsoredFamilies } from "./../../../billing/guards/can-access-sponsored-families.guard"; -import { MembersComponent } from "./deprecated_members.component"; -import { vNextMembersComponent } from "./members.component"; +import { MembersComponent } from "./members.component"; const routes: Routes = [ - ...featureFlaggedRoute({ - defaultComponent: MembersComponent, - flaggedComponent: vNextMembersComponent, - featureFlag: FeatureFlag.MembersComponentRefactor, - routeOptions: { - path: "", - canActivate: [organizationPermissionsGuard(canAccessMembersTab)], - data: { - titleId: "members", - }, + { + path: "", + component: MembersComponent, + canActivate: [organizationPermissionsGuard(canAccessMembersTab)], + data: { + titleId: "members", }, - }), + }, { path: "sponsored-families", component: FreeBitwardenFamiliesComponent, diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.spec.ts b/apps/web/src/app/admin-console/organizations/members/members.component.spec.ts index 1cd90989b12..72c12fd4d79 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.spec.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.spec.ts @@ -36,7 +36,7 @@ import { OrganizationUserView } from "../core/views/organization-user.view"; import { AccountRecoveryDialogResultType } from "./components/account-recovery/account-recovery-dialog.component"; import { MemberDialogResult } from "./components/member-dialog"; -import { vNextMembersComponent } from "./members.component"; +import { MembersComponent } from "./members.component"; import { MemberDialogManagerService, MemberExportService, @@ -48,9 +48,9 @@ import { MemberActionResult, } from "./services/member-actions/member-actions.service"; -describe("vNextMembersComponent", () => { - let component: vNextMembersComponent; - let fixture: ComponentFixture; +describe("MembersComponent", () => { + let component: MembersComponent; + let fixture: ComponentFixture; let mockApiService: MockProxy; let mockI18nService: MockProxy; @@ -172,7 +172,7 @@ describe("vNextMembersComponent", () => { mockFileDownloadService = mock(); await TestBed.configureTestingModule({ - declarations: [vNextMembersComponent], + declarations: [MembersComponent], providers: [ { provide: ApiService, useValue: mockApiService }, { provide: I18nService, useValue: mockI18nService }, @@ -211,13 +211,13 @@ describe("vNextMembersComponent", () => { ], schemas: [NO_ERRORS_SCHEMA], }) - .overrideComponent(vNextMembersComponent, { + .overrideComponent(MembersComponent, { remove: { imports: [] }, add: { template: "
" }, }) .compileComponents(); - fixture = TestBed.createComponent(vNextMembersComponent); + fixture = TestBed.createComponent(MembersComponent); component = fixture.componentInstance; fixture.detectChanges(); }); @@ -515,7 +515,7 @@ describe("vNextMembersComponent", () => { }; jest.spyOn(component["dataSource"](), "isIncreasedBulkLimitEnabled").mockReturnValue(false); jest.spyOn(component["dataSource"](), "getCheckedUsers").mockReturnValue([invitedUser]); - mockMemberActionsService.bulkReinvite.mockResolvedValue({ successful: true }); + mockMemberActionsService.bulkReinvite.mockResolvedValue({ successful: [{}], failed: [] }); await component.bulkReinvite(mockOrg); @@ -549,7 +549,7 @@ describe("vNextMembersComponent", () => { jest.spyOn(component["dataSource"](), "isIncreasedBulkLimitEnabled").mockReturnValue(false); jest.spyOn(component["dataSource"](), "getCheckedUsers").mockReturnValue([invitedUser]); const error = new Error("Bulk reinvite failed"); - mockMemberActionsService.bulkReinvite.mockResolvedValue({ successful: false, failed: error }); + mockMemberActionsService.bulkReinvite.mockResolvedValue({ successful: [], failed: error }); await component.bulkReinvite(mockOrg); diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 6139c5f07a5..6b93edc8c6b 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -82,7 +82,7 @@ interface BulkMemberFlags { templateUrl: "members.component.html", standalone: false, }) -export class vNextMembersComponent { +export class MembersComponent { protected i18nService = inject(I18nService); protected validationService = inject(ValidationService); protected logService = inject(LogService); @@ -426,7 +426,7 @@ export class vNextMembersComponent { const result = await this.memberActionsService.bulkReinvite(organization, filteredUsers); - if (!result.successful) { + if (result.successful.length === 0) { this.validationService.showError(result.failed); } diff --git a/apps/web/src/app/admin-console/organizations/members/members.module.ts b/apps/web/src/app/admin-console/organizations/members/members.module.ts index 54e2d1b6373..92ae71123cc 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.module.ts @@ -19,9 +19,8 @@ import { BulkRemoveDialogComponent } from "./components/bulk/bulk-remove-dialog. import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component"; import { BulkStatusComponent } from "./components/bulk/bulk-status.component"; import { UserDialogModule } from "./components/member-dialog"; -import { MembersComponent } from "./deprecated_members.component"; import { MembersRoutingModule } from "./members-routing.module"; -import { vNextMembersComponent } from "./members.component"; +import { MembersComponent } from "./members.component"; import { UserStatusPipe } from "./pipes"; import { OrganizationMembersService, @@ -52,7 +51,6 @@ import { BulkProgressDialogComponent, BulkReinviteFailureDialogComponent, MembersComponent, - vNextMembersComponent, BulkDeleteDialogComponent, UserStatusPipe, ], diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts index 688c7ed77ce..1ba056a24f6 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts @@ -507,7 +507,7 @@ describe("MemberActionsService", () => { const result = await service.bulkReinvite(mockOrganization, users); - expect(result.successful).toBeUndefined(); + expect(result.successful).toHaveLength(0); expect(result.failed).toHaveLength(totalUsers); expect(result.failed.every((f) => f.error === errorMessage)).toBe(true); expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledTimes(2); diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts index e5f8c0c6673..7d573c8eeef 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts @@ -37,11 +37,7 @@ export interface MemberActionResult { } export class BulkActionResult { - constructor() { - this.failed = []; - } - - successful?: OrganizationUserBulkResponse[]; + successful: OrganizationUserBulkResponse[] = []; failed: { id: string; error: string }[] = []; } @@ -316,7 +312,7 @@ export class MemberActionsService { } return { - successful: allSuccessful.length > 0 ? allSuccessful : undefined, + successful: allSuccessful, failed: allFailed, }; } diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-dialog-manager/member-dialog-manager.service.ts b/apps/web/src/app/admin-console/organizations/members/services/member-dialog-manager/member-dialog-manager.service.ts index 18106031fd0..6c367692376 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/member-dialog-manager/member-dialog-manager.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/member-dialog-manager/member-dialog-manager.service.ts @@ -1,8 +1,10 @@ import { Injectable, WritableSignal } from "@angular/core"; import { firstValueFrom, lastValueFrom } from "rxjs"; +import { OrganizationUserBulkResponse } from "@bitwarden/admin-console/common"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -197,7 +199,7 @@ export class MemberDialogManagerService { async openBulkStatusDialog( users: OrganizationUserView[], filteredUsers: OrganizationUserView[], - request: Promise, + request: Promise, successMessage: string, ): Promise { const dialogRef = BulkStatusComponent.open(this.dialogService, { diff --git a/apps/web/src/app/billing/clients/account-billing.client.ts b/apps/web/src/app/billing/clients/account-billing.client.ts index 1334ff643dd..6864e1de981 100644 --- a/apps/web/src/app/billing/clients/account-billing.client.ts +++ b/apps/web/src/app/billing/clients/account-billing.client.ts @@ -4,6 +4,8 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { BitwardenSubscriptionResponse } from "@bitwarden/common/billing/models/response/bitwarden-subscription.response"; import { SubscriptionCadence } from "@bitwarden/common/billing/types/subscription-pricing-tier"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { Maybe } from "@bitwarden/pricing"; import { BitwardenSubscription } from "@bitwarden/subscription"; import { @@ -23,11 +25,18 @@ export class AccountBillingClient { return this.apiService.send("GET", path, null, true, true); }; - getSubscription = async (): Promise => { + getSubscription = async (): Promise> => { const path = `${this.endpoint}/subscription`; - const json = await this.apiService.send("GET", path, null, true, true); - const response = new BitwardenSubscriptionResponse(json); - return response.toDomain(); + try { + const json = await this.apiService.send("GET", path, null, true, true); + const response = new BitwardenSubscriptionResponse(json); + return response.toDomain(); + } catch (error: any) { + if (error instanceof ErrorResponse && error.statusCode === 404) { + return null; + } + throw error; + } }; purchaseSubscription = async ( 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 f85dab54fe7..8d9c999caec 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 @@ -19,7 +19,7 @@ const routes: Routes = [ component: SubscriptionComponent, data: { titleId: "subscription" }, children: [ - { path: "", pathMatch: "full", redirectTo: "premium" }, + { path: "", pathMatch: "full", redirectTo: "user-subscription" }, ...featureFlaggedRoute({ defaultComponent: UserSubscriptionComponent, flaggedComponent: AccountSubscriptionComponent, diff --git a/apps/web/src/app/billing/individual/subscription.component.ts b/apps/web/src/app/billing/individual/subscription.component.ts index 37fb2baf3a6..4f52f3c2ea2 100644 --- a/apps/web/src/app/billing/individual/subscription.component.ts +++ b/apps/web/src/app/billing/individual/subscription.component.ts @@ -1,17 +1,22 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; -import { Observable, switchMap } from "rxjs"; +import { combineLatest, from, map, Observable, switchMap } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { AccountBillingClient } from "../clients/account-billing.client"; + // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "subscription.component.html", standalone: false, + providers: [AccountBillingClient], }) export class SubscriptionComponent implements OnInit { hasPremium$: Observable; @@ -21,9 +26,21 @@ export class SubscriptionComponent implements OnInit { private platformUtilsService: PlatformUtilsService, billingAccountProfileStateService: BillingAccountProfileStateService, accountService: AccountService, + configService: ConfigService, + private accountBillingClient: AccountBillingClient, ) { - this.hasPremium$ = accountService.activeAccount$.pipe( - switchMap((account) => billingAccountProfileStateService.hasPremiumPersonally$(account.id)), + this.hasPremium$ = combineLatest([ + configService.getFeatureFlag$(FeatureFlag.PM29594_UpdateIndividualSubscriptionPage), + accountService.activeAccount$, + ]).pipe( + switchMap(([isFeatureFlagEnabled, account]) => { + if (isFeatureFlagEnabled) { + return from(accountBillingClient.getSubscription()).pipe( + map((subscription) => !!subscription), + ); + } + return billingAccountProfileStateService.hasPremiumPersonally$(account.id); + }), ); } diff --git a/apps/web/src/app/billing/individual/subscription/account-subscription.component.ts b/apps/web/src/app/billing/individual/subscription/account-subscription.component.ts index d8e25de7965..7fdc830effd 100644 --- a/apps/web/src/app/billing/individual/subscription/account-subscription.component.ts +++ b/apps/web/src/app/billing/individual/subscription/account-subscription.component.ts @@ -34,6 +34,11 @@ import { AdjustAccountSubscriptionStorageDialogComponent, AdjustAccountSubscriptionStorageDialogParams, } from "@bitwarden/web-vault/app/billing/individual/subscription/adjust-account-subscription-storage-dialog.component"; +import { + UnifiedUpgradeDialogComponent, + UnifiedUpgradeDialogStatus, + UnifiedUpgradeDialogStep, +} from "@bitwarden/web-vault/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component"; import { OffboardingSurveyDialogResultType, openOffboardingSurvey, @@ -93,10 +98,11 @@ export class AccountSubscriptionComponent { if (!this.account()) { return await redirectToPremiumPage(); } - if (!this.hasPremiumPersonally()) { + const subscription = await this.accountBillingClient.getSubscription(); + if (!subscription) { return await redirectToPremiumPage(); } - return await this.accountBillingClient.getSubscription(); + return subscription; }, }); @@ -106,6 +112,7 @@ export class AccountSubscriptionComponent { const subscription = this.subscription.value(); if (subscription) { return ( + subscription.status === SubscriptionStatuses.Incomplete || subscription.status === SubscriptionStatuses.IncompleteExpired || subscription.status === SubscriptionStatuses.Canceled || subscription.status === SubscriptionStatuses.Unpaid @@ -230,6 +237,27 @@ export class AccountSubscriptionComponent { case SubscriptionCardActions.UpdatePayment: await this.router.navigate(["../payment-details"], { relativeTo: this.activatedRoute }); break; + case SubscriptionCardActions.Resubscribe: { + const account = this.account(); + if (!account) { + return; + } + + const dialogRef = UnifiedUpgradeDialogComponent.open(this.dialogService, { + data: { + account, + initialStep: UnifiedUpgradeDialogStep.Payment, + selectedPlan: PersonalSubscriptionPricingTierIds.Premium, + }, + }); + + const result = await lastValueFrom(dialogRef.closed); + + if (result?.status === UnifiedUpgradeDialogStatus.UpgradedToPremium) { + this.subscription.reload(); + } + break; + } case SubscriptionCardActions.UpgradePlan: await this.openUpgradeDialog(); break; 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 9862f62c2e2..44039bfe605 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 @@ -7,11 +7,11 @@ region == currentRegion ? 'javascript:void(0)' : region.urls.webVault + routeAndParams " > - + > {{ region.domain }} @@ -19,7 +19,7 @@ {{ "accessing" | i18n }}: {{ currentRegion?.domain }} - + diff --git a/apps/web/src/app/layouts/header/web-header.component.html b/apps/web/src/app/layouts/header/web-header.component.html index 995169e3dc1..9288c96237e 100644 --- a/apps/web/src/app/layouts/header/web-header.component.html +++ b/apps/web/src/app/layouts/header/web-header.component.html @@ -60,11 +60,11 @@ - + {{ "accountSettings" | i18n }} - + {{ "getHelp" | i18n }} - + {{ "getApps" | i18n }} 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 a9acddeb0b8..b8f7c5ab0c0 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 @@ -7,13 +7,14 @@ [routerLinkActiveOptions]="{ exact: true }" [(open)]="open" > - + > - + > -
{{ fingerprint }} diff --git a/apps/web/src/app/shared/components/onboarding/onboarding-task.component.html b/apps/web/src/app/shared/components/onboarding/onboarding-task.component.html index f0c0b01e06e..e52771a282b 100644 --- a/apps/web/src/app/shared/components/onboarding/onboarding-task.component.html +++ b/apps/web/src/app/shared/components/onboarding/onboarding-task.component.html @@ -1,10 +1,14 @@ - {{ title }}{{ title }} diff --git a/apps/web/src/app/shared/components/onboarding/onboarding-task.component.ts b/apps/web/src/app/shared/components/onboarding/onboarding-task.component.ts index 277a4d2d26e..47a618a1269 100644 --- a/apps/web/src/app/shared/components/onboarding/onboarding-task.component.ts +++ b/apps/web/src/app/shared/components/onboarding/onboarding-task.component.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Component, Input } from "@angular/core"; +import { BitwardenIcon } from "@bitwarden/components"; + // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ @@ -21,7 +23,7 @@ export class OnboardingTaskComponent { // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals @Input() - icon = "bwi-info-circle"; + icon: BitwardenIcon = "bwi-info-circle"; // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals diff --git a/apps/web/src/app/shared/components/onboarding/onboarding.component.html b/apps/web/src/app/shared/components/onboarding/onboarding.component.html index 2433ec51fcc..ca98ceb8fbf 100644 --- a/apps/web/src/app/shared/components/onboarding/onboarding.component.html +++ b/apps/web/src/app/shared/components/onboarding/onboarding.component.html @@ -6,11 +6,7 @@ {{ "complete" | i18n: amountCompleted : tasks.length }} - +
    @@ -24,5 +20,5 @@ - + diff --git a/apps/web/src/app/shared/components/onboarding/onboarding.stories.ts b/apps/web/src/app/shared/components/onboarding/onboarding.stories.ts index 6873700e2bc..26c951fb11f 100644 --- a/apps/web/src/app/shared/components/onboarding/onboarding.stories.ts +++ b/apps/web/src/app/shared/components/onboarding/onboarding.stories.ts @@ -4,7 +4,7 @@ import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/an import { delay, of, startWith } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { LinkModule, SvgModule, ProgressModule } from "@bitwarden/components"; +import { LinkModule, SvgModule, ProgressModule, IconModule } from "@bitwarden/components"; import { PreloadedEnglishI18nModule } from "../../../core/tests"; @@ -16,7 +16,7 @@ export default { component: OnboardingComponent, decorators: [ moduleMetadata({ - imports: [JslibModule, RouterModule, LinkModule, SvgModule, ProgressModule], + imports: [JslibModule, RouterModule, LinkModule, IconModule, SvgModule, ProgressModule], declarations: [OnboardingTaskComponent], }), applicationConfig({ diff --git a/apps/web/src/app/shared/shared.module.ts b/apps/web/src/app/shared/shared.module.ts index b83555fd84e..729238e0b0d 100644 --- a/apps/web/src/app/shared/shared.module.ts +++ b/apps/web/src/app/shared/shared.module.ts @@ -18,6 +18,7 @@ import { DialogModule, FormFieldModule, IconButtonModule, + IconModule, SvgModule, LinkModule, MenuModule, @@ -63,6 +64,7 @@ import { DialogModule, FormFieldModule, IconButtonModule, + IconModule, SvgModule, LinkModule, MenuModule, @@ -99,6 +101,7 @@ import { DialogModule, FormFieldModule, IconButtonModule, + IconModule, SvgModule, LinkModule, MenuModule, diff --git a/apps/web/src/app/tools/send/send-access/send-auth.component.ts b/apps/web/src/app/tools/send/send-access/send-auth.component.ts index 9ed8106ad40..8c630ce5315 100644 --- a/apps/web/src/app/tools/send/send-access/send-auth.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-auth.component.ts @@ -26,7 +26,7 @@ import { SendAccessResponse } from "@bitwarden/common/tools/send/models/response import { SEND_KDF_ITERATIONS } from "@bitwarden/common/tools/send/send-kdf"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { AuthType } from "@bitwarden/common/tools/send/types/auth-type"; -import { ToastService } from "@bitwarden/components"; +import { AnonLayoutWrapperDataService, ToastService } from "@bitwarden/components"; import { SharedModule } from "../../../shared"; @@ -69,6 +69,7 @@ export class SendAuthComponent implements OnInit { private formBuilder: FormBuilder, private configService: ConfigService, private sendTokenService: SendTokenService, + private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, ) {} ngOnInit() { @@ -160,8 +161,10 @@ export class SendAuthComponent implements OnInit { this.expiredAuthAttempts = 0; if (emailRequired(response.error)) { this.sendAuthType.set(AuthType.Email); + this.updatePageTitle(); } else if (emailAndOtpRequired(response.error)) { this.enterOtp.set(true); + this.updatePageTitle(); } else if (otpInvalid(response.error)) { this.toastService.showToast({ variant: "error", @@ -170,6 +173,7 @@ export class SendAuthComponent implements OnInit { }); } else if (passwordHashB64Required(response.error)) { this.sendAuthType.set(AuthType.Password); + this.updatePageTitle(); } else if (passwordHashB64Invalid(response.error)) { this.toastService.showToast({ variant: "error", @@ -207,4 +211,24 @@ export class SendAuthComponent implements OnInit { ); return Utils.fromBufferToB64(passwordHash) as SendHashedPasswordB64; } + + private updatePageTitle(): void { + const authType = this.sendAuthType(); + + if (authType === AuthType.Email) { + if (this.enterOtp()) { + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { key: "enterTheCodeSentToYourEmail" }, + }); + } else { + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { key: "verifyYourEmailToViewThisSend" }, + }); + } + } else if (authType === AuthType.Password) { + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { key: "sendAccessPasswordTitle" }, + }); + } + } } diff --git a/apps/web/src/app/tools/send/send-access/send-view.component.ts b/apps/web/src/app/tools/send/send-access/send-view.component.ts index 1ab9a121ace..923a749db92 100644 --- a/apps/web/src/app/tools/send/send-access/send-view.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-view.component.ts @@ -69,6 +69,9 @@ export class SendViewComponent implements OnInit { ) {} ngOnInit() { + this.layoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { key: "sendAccessContentTitle" }, + }); void this.load(); } diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index 4da2d05f12b..4c6efdee167 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -100,7 +100,7 @@ export interface VaultItemDialogParams { /** * Function to restore a cipher from the trash. */ - restore?: (c: CipherViewLike) => Promise; + restore?: (c: CipherViewLike) => Promise; } export const VaultItemDialogResult = { @@ -616,7 +616,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { this.toastService.showToast({ variant: "success", - message: this.i18nService.t("itemWasSentToArchive"), + message: this.i18nService.t("itemArchiveToast"), }); } catch { this.toastService.showToast({ @@ -638,7 +638,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { this.toastService.showToast({ variant: "success", - message: this.i18nService.t("itemWasUnarchived"), + message: this.i18nService.t("itemUnarchivedToast"), }); } catch { this.toastService.showToast({ diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 4b9d2ed59ee..5ff72b0d147 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -1,15 +1,6 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core"; +import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, viewChild } from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; -import { - BehaviorSubject, - combineLatest, - firstValueFrom, - lastValueFrom, - Observable, - Subject, -} from "rxjs"; +import { combineLatest, firstValueFrom, lastValueFrom, Observable, of, Subject } from "rxjs"; import { concatMap, debounceTime, @@ -18,6 +9,7 @@ import { first, map, shareReplay, + startWith, switchMap, take, takeUntil, @@ -89,7 +81,6 @@ import { CipherListView } from "@bitwarden/sdk-internal"; import { AddEditFolderDialogComponent, AddEditFolderDialogResult, - AttachmentDialogCloseResult, AttachmentDialogResult, AttachmentsV2Component, CipherFormConfig, @@ -179,14 +170,10 @@ type EmptyStateMap = Record; ], }) export class VaultComponent implements OnInit, OnDestroy { - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @ViewChild("vaultItems", { static: false }) vaultItemsComponent: VaultItemsComponent; + readonly filterComponent = viewChild(VaultFilterComponent); + readonly vaultItemsComponent = viewChild(VaultItemsComponent); - trashCleanupWarning: string = null; + trashCleanupWarning: string = ""; activeFilter: VaultFilter = new VaultFilter(); protected deactivatedOrgIcon = DeactivatedOrg; @@ -198,20 +185,20 @@ export class VaultComponent implements OnInit, OnDestr protected refreshing = false; protected processingEvent = false; protected filter: RoutedVaultFilterModel = {}; - protected showBulkMove: boolean; - protected canAccessPremium: boolean; - protected allCollections: CollectionView[]; + protected showBulkMove: boolean = false; + protected canAccessPremium: boolean = false; + protected allCollections: CollectionView[] = []; protected allOrganizations: Organization[] = []; - protected ciphers: C[]; - protected collections: CollectionView[]; - protected isEmpty: boolean; + protected ciphers: C[] = []; + protected collections: CollectionView[] = []; + protected isEmpty: boolean = false; protected selectedCollection: TreeNode | undefined; protected canCreateCollections = false; protected currentSearchText$: Observable = this.route.queryParams.pipe( map((queryParams) => queryParams.search), ); private searchText$ = new Subject(); - private refresh$ = new BehaviorSubject(null); + private refresh$ = new Subject(); private destroy$ = new Subject(); private vaultItemDialogRef?: DialogRef | undefined; @@ -220,7 +207,7 @@ export class VaultComponent implements OnInit, OnDestr organizations$ = this.accountService.activeAccount$ .pipe(map((a) => a?.id)) - .pipe(switchMap((id) => this.organizationService.organizations$(id))); + .pipe(switchMap((id) => (id ? this.organizationService.organizations$(id) : of([])))); emptyState$ = combineLatest([ this.currentSearchText$, @@ -228,7 +215,7 @@ export class VaultComponent implements OnInit, OnDestr this.organizations$, ]).pipe( map(([searchText, filter, organizations]) => { - const selectedOrg = organizations?.find((org) => org.id === filter.organizationId); + const selectedOrg = organizations.find((org) => org.id === filter.organizationId); const isOrgDisabled = selectedOrg && !selectedOrg.enabled; if (isOrgDisabled) { @@ -586,7 +573,7 @@ export class VaultComponent implements OnInit, OnDestr firstSetup$ .pipe( - switchMap(() => this.refresh$), + switchMap(() => this.refresh$.pipe(startWith(undefined))), tap(() => (this.refreshing = true)), switchMap(() => combineLatest([ @@ -712,7 +699,6 @@ export class VaultComponent implements OnInit, OnDestr async handleUnknownCipher() { this.toastService.showToast({ variant: "error", - title: null, message: this.i18nService.t("unknownCipher"), }); await this.router.navigate([], { @@ -744,7 +730,7 @@ export class VaultComponent implements OnInit, OnDestr await this.cipherArchiveService.archiveWithServer(cipher.id as CipherId, activeUserId); this.toastService.showToast({ variant: "success", - message: this.i18nService.t("itemWasSentToArchive"), + message: this.i18nService.t("itemArchiveToast"), }); this.refresh(); } catch (e) { @@ -801,7 +787,7 @@ export class VaultComponent implements OnInit, OnDestr this.toastService.showToast({ variant: "success", - message: this.i18nService.t("itemUnarchived"), + message: this.i18nService.t("itemUnarchivedToast"), }); this.refresh(); @@ -842,9 +828,13 @@ export class VaultComponent implements OnInit, OnDestr if (orgId == null) { orgId = "MyVault"; } - const orgs = await firstValueFrom(this.filterComponent.filters.organizationFilter.data$); + const data = this.filterComponent()?.filters?.organizationFilter?.data$; + if (data == undefined) { + return; + } + const orgs = await firstValueFrom(data); const orgNode = ServiceUtils.getTreeNodeObject(orgs, orgId) as TreeNode; - await this.filterComponent.filters?.organizationFilter?.action(orgNode); + await this.filterComponent()?.filters?.organizationFilter?.action(orgNode); } addFolder = (): void => { @@ -912,7 +902,10 @@ export class VaultComponent implements OnInit, OnDestr canEditCipher: cipher.edit, }); - const result: AttachmentDialogCloseResult = await lastValueFrom(dialogRef.closed); + const result = await lastValueFrom(dialogRef.closed); + if (result === undefined) { + return; + } if ( result.action === AttachmentDialogResult.Uploaded || @@ -966,7 +959,7 @@ export class VaultComponent implements OnInit, OnDestr */ async addCipher(cipherType?: CipherType) { const type = cipherType ?? this.activeFilter.cipherType; - const cipherFormConfig = await this.cipherFormConfigService.buildConfig("add", null, type); + const cipherFormConfig = await this.cipherFormConfigService.buildConfig("add", undefined, type); const collectionId = this.activeFilter.collectionId !== "AllCollections" && this.activeFilter.collectionId != null ? this.activeFilter.collectionId @@ -994,7 +987,7 @@ export class VaultComponent implements OnInit, OnDestr } async editCipher(cipher: CipherView | CipherListView, cloneMode?: boolean) { - return this.editCipherId(uuidAsString(cipher?.id), cloneMode); + return this.editCipherId(uuidAsString(cipher.id), cloneMode); } /** @@ -1088,6 +1081,9 @@ export class VaultComponent implements OnInit, OnDestr }, }); const result = await lastValueFrom(dialog.closed); + if (result === undefined) { + return; + } if (result.action === CollectionDialogAction.Saved) { if (result.collection) { // Update CollectionService with the new collection @@ -1104,7 +1100,7 @@ export class VaultComponent implements OnInit, OnDestr async editCollection(c: CollectionView, tab: CollectionDialogTabType): Promise { const dialog = openCollectionDialog(this.dialogService, { data: { - collectionId: c?.id, + collectionId: c.id, organizationId: c.organizationId, initialTab: tab, limitNestedCollections: true, @@ -1112,6 +1108,9 @@ export class VaultComponent implements OnInit, OnDestr }); const result = await lastValueFrom(dialog.closed); + if (result === undefined) { + return; + } const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); if (result.action === CollectionDialogAction.Saved) { if (result.collection) { @@ -1163,7 +1162,6 @@ export class VaultComponent implements OnInit, OnDestr this.toastService.showToast({ variant: "success", - title: null, message: this.i18nService.t("deletedCollectionId", collection.name), }); if (navigateAway) { @@ -1196,12 +1194,12 @@ export class VaultComponent implements OnInit, OnDestr let availableCollections: CollectionView[] = []; const orgId = this.activeFilter.organizationId || - ciphers.find((c) => c.organizationId !== null)?.organizationId; + ciphers.find((c) => c.organizationId !== undefined)?.organizationId; if (orgId && orgId !== "MyVault") { const organization = this.allOrganizations.find((o) => o.id === orgId); availableCollections = this.allCollections.filter( - (c) => c.organizationId === organization.id, + (c) => c.organizationId === organization?.id, ); } @@ -1229,7 +1227,7 @@ export class VaultComponent implements OnInit, OnDestr ciphers: ciphersToAssign, organizationId: orgId as OrganizationId, availableCollections, - activeCollection: this.activeFilter?.selectedCollectionNode?.node, + activeCollection: this.activeFilter.selectedCollectionNode?.node, }, }); @@ -1255,7 +1253,7 @@ export class VaultComponent implements OnInit, OnDestr await this.editCipher(cipher, true); } - restore = async (c: C): Promise => { + restore = async (c: CipherViewLike) => { let toastMessage; if (!CipherViewLikeUtils.isDeleted(c)) { return; @@ -1281,13 +1279,14 @@ export class VaultComponent implements OnInit, OnDestr await this.cipherService.restoreWithServer(uuidAsString(c.id), activeUserId); this.toastService.showToast({ variant: "success", - title: null, message: toastMessage, }); this.refresh(); } catch (e) { this.logService.error(e); + return; } + return; }; async bulkRestore(ciphers: C[]) { @@ -1311,7 +1310,6 @@ export class VaultComponent implements OnInit, OnDestr if (selectedCipherIds.length === 0) { this.toastService.showToast({ variant: "error", - title: null, message: this.i18nService.t("nothingSelected"), }); return; @@ -1321,23 +1319,24 @@ export class VaultComponent implements OnInit, OnDestr await this.cipherService.restoreManyWithServer(selectedCipherIds, activeUserId); this.toastService.showToast({ variant: "success", - title: null, message: toastMessage, }); this.refresh(); } private async handleDeleteEvent(items: VaultItem[]) { - const ciphers: C[] = items.filter((i) => i.collection === undefined).map((i) => i.cipher); - const collections = items.filter((i) => i.cipher === undefined).map((i) => i.collection); + const ciphers = items + .filter((i) => i.collection === undefined && i.cipher !== undefined) + .map((i) => i.cipher as C); + const collections = items + .filter((i) => i.collection !== undefined) + .map((i) => i.collection as CollectionView); if (ciphers.length === 1 && collections.length === 0) { await this.deleteCipher(ciphers[0]); } else if (ciphers.length === 0 && collections.length === 1) { await this.deleteCollection(collections[0]); } else { - const orgIds = items - .filter((i) => i.cipher === undefined) - .map((i) => i.collection.organizationId); + const orgIds = collections.map((c) => c.organizationId); const orgs = await firstValueFrom( this.organizations$.pipe(map((orgs) => orgs.filter((o) => orgIds.includes(o.id)))), ); @@ -1345,7 +1344,7 @@ export class VaultComponent implements OnInit, OnDestr } } - async deleteCipher(c: C): Promise { + async deleteCipher(c: C) { if (!(await this.repromptCipher([c]))) { return; } @@ -1364,7 +1363,7 @@ export class VaultComponent implements OnInit, OnDestr }); if (!confirmed) { - return false; + return; } try { @@ -1373,7 +1372,6 @@ export class VaultComponent implements OnInit, OnDestr this.toastService.showToast({ variant: "success", - title: null, message: this.i18nService.t(permanent ? "permanentlyDeletedItem" : "deletedItem"), }); this.refresh(); @@ -1390,7 +1388,6 @@ export class VaultComponent implements OnInit, OnDestr if (ciphers.length === 0 && collections.length === 0) { this.toastService.showToast({ variant: "error", - title: null, message: this.i18nService.t("nothingSelected"), }); return; @@ -1430,7 +1427,6 @@ export class VaultComponent implements OnInit, OnDestr if (selectedCipherIds.length === 0) { this.toastService.showToast({ variant: "error", - title: null, message: this.i18nService.t("nothingSelected"), }); return; @@ -1454,11 +1450,8 @@ export class VaultComponent implements OnInit, OnDestr const login = CipherViewLikeUtils.getLogin(cipher); if (!login) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("unexpectedError"), - }); + this.showErrorToast(); + return; } if (field === "username") { @@ -1471,15 +1464,15 @@ export class VaultComponent implements OnInit, OnDestr typeI18nKey = "password"; } else if (field === "totp") { aType = "TOTP"; + if (!login.totp) { + this.showErrorToast(); + return; + } const totpResponse = await firstValueFrom(this.totpService.getCode$(login.totp)); value = totpResponse.code; typeI18nKey = "verificationCodeTotp"; } else { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("unexpectedError"), - }); + this.showErrorToast(); return; } @@ -1494,10 +1487,13 @@ export class VaultComponent implements OnInit, OnDestr return; } + if (!value) { + this.showErrorToast(); + return; + } this.platformUtilsService.copyToClipboard(value, { window: window }); this.toastService.showToast({ variant: "info", - title: null, message: this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), }); @@ -1514,6 +1510,13 @@ export class VaultComponent implements OnInit, OnDestr } } + showErrorToast() { + this.toastService.showToast({ + variant: "error", + message: this.i18nService.t("unexpectedError"), + }); + } + /** * Toggles the favorite status of the cipher and updates it on the server. */ @@ -1525,7 +1528,6 @@ export class VaultComponent implements OnInit, OnDestr this.toastService.showToast({ variant: "success", - title: null, message: this.i18nService.t( cipherFullView.favorite ? "itemAddedToFavorites" : "itemRemovedFromFavorites", ), @@ -1540,15 +1542,15 @@ export class VaultComponent implements OnInit, OnDestr : this.cipherService.softDeleteWithServer(id, userId); } - protected async repromptCipher(ciphers: C[]) { + protected async repromptCipher(ciphers: CipherViewLike[]) { const notProtected = !ciphers.find((cipher) => cipher.reprompt !== CipherRepromptType.None); return notProtected || (await this.passwordRepromptService.showPasswordPrompt()); } private refresh() { - this.refresh$.next(); - this.vaultItemsComponent?.clearSelection(); + this.refresh$.next(undefined); + this.vaultItemsComponent()?.clearSelection(); } private async go(queryParams: any = null) { @@ -1573,7 +1575,6 @@ export class VaultComponent implements OnInit, OnDestr private showMissingPermissionsError() { this.toastService.showToast({ variant: "error", - title: null, message: this.i18nService.t("missingPermissions"), }); } @@ -1584,13 +1585,13 @@ export class VaultComponent implements OnInit, OnDestr */ private async getPasswordFromCipherViewLike(cipher: C): Promise { if (!CipherViewLikeUtils.isCipherListView(cipher)) { - return Promise.resolve(cipher.login?.password); + return Promise.resolve(cipher?.login?.password); } const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const _cipher = await this.cipherService.get(uuidAsString(cipher.id), activeUserId); const cipherView = await this.cipherService.decrypt(_cipher, activeUserId); - return cipherView.login?.password; + return cipherView.login.password; } } diff --git a/apps/web/src/connectors/platform/proxy-cookie-redirect.html b/apps/web/src/connectors/platform/proxy-cookie-redirect.html index 1daa6d2e412..1918fcd771c 100644 --- a/apps/web/src/connectors/platform/proxy-cookie-redirect.html +++ b/apps/web/src/connectors/platform/proxy-cookie-redirect.html @@ -18,6 +18,7 @@
    Bitwarden
    +
    + - - - - - -
    - - - {{ "all" | i18n }} - - {{ allCount }} - - - - {{ "invited" | i18n }} - - {{ invitedCount }} - - - - {{ "needsConfirmation" | i18n }} - - {{ acceptedCount }} - - - -
    - - - - {{ "loading" | i18n }} - - - -

    {{ "noMembersInList" | i18n }}

    - - - {{ "providerUsersNeedConfirmed" | i18n }} - - - - - - - - - - {{ "name" | i18n }} - {{ "role" | i18n }} - - - - - - - - - - - - - - - - -
    - -
    -
    - - - {{ "invited" | i18n }} - - - {{ "needsConfirmation" | i18n }} - - - {{ "revoked" | i18n }} - -
    -
    - {{ user.email }} -
    -
    -
    - - - {{ "providerAdmin" | i18n }} - {{ "serviceUser" | i18n }} - - - - - - - - - - - -
    -
    -
    -
    -
    diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.ts deleted file mode 100644 index 1b1ae25c027..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.ts +++ /dev/null @@ -1,349 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { ActivatedRoute, Router } from "@angular/router"; -import { combineLatest, firstValueFrom, lastValueFrom, switchMap } from "rxjs"; -import { first, map } from "rxjs/operators"; - -import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; -import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { ProviderUserStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; -import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request"; -import { ProviderUserConfirmRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-confirm.request"; -import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { assertNonNullish } from "@bitwarden/common/auth/utils"; -import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; -import { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { ProviderId } from "@bitwarden/common/types/guid"; -import { DialogRef, DialogService, ToastService } from "@bitwarden/components"; -import { KeyService } from "@bitwarden/key-management"; -import { BaseMembersComponent } from "@bitwarden/web-vault/app/admin-console/common/base-members.component"; -import { - CloudBulkReinviteLimit, - MaxCheckedCount, - peopleFilter, - PeopleTableDataSource, -} from "@bitwarden/web-vault/app/admin-console/common/people-table-data-source"; -import { openEntityEventsDialog } from "@bitwarden/web-vault/app/admin-console/organizations/manage/entity-events.component"; -import { BulkStatusComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-status.component"; -import { MemberActionResult } from "@bitwarden/web-vault/app/admin-console/organizations/members/services/member-actions/member-actions.service"; - -import { - AddEditMemberDialogComponent, - AddEditMemberDialogParams, - AddEditMemberDialogResultType, -} from "./dialogs/add-edit-member-dialog.component"; -import { BulkConfirmDialogComponent } from "./dialogs/bulk-confirm-dialog.component"; -import { BulkRemoveDialogComponent } from "./dialogs/bulk-remove-dialog.component"; - -type ProviderUser = ProviderUserUserDetailsResponse; - -class MembersTableDataSource extends PeopleTableDataSource { - protected statusType = ProviderUserStatusType; -} - -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -@Component({ - templateUrl: "deprecated_members.component.html", - standalone: false, -}) -export class MembersComponent extends BaseMembersComponent { - accessEvents = false; - dataSource: MembersTableDataSource; - loading = true; - providerId: string; - rowHeight = 70; - rowHeightClass = `tw-h-[70px]`; - status: ProviderUserStatusType = null; - - userStatusType = ProviderUserStatusType; - userType = ProviderUserType; - - constructor( - apiService: ApiService, - keyService: KeyService, - dialogService: DialogService, - i18nService: I18nService, - logService: LogService, - organizationManagementPreferencesService: OrganizationManagementPreferencesService, - toastService: ToastService, - userNamePipe: UserNamePipe, - validationService: ValidationService, - private encryptService: EncryptService, - private activatedRoute: ActivatedRoute, - private providerService: ProviderService, - private router: Router, - private accountService: AccountService, - private environmentService: EnvironmentService, - ) { - super( - apiService, - i18nService, - keyService, - validationService, - logService, - userNamePipe, - dialogService, - organizationManagementPreferencesService, - toastService, - ); - - this.dataSource = new MembersTableDataSource(this.environmentService); - - combineLatest([ - this.activatedRoute.parent.params, - this.activatedRoute.queryParams.pipe(first()), - ]) - .pipe( - switchMap(async ([urlParams, queryParams]) => { - this.searchControl.setValue(queryParams.search); - this.dataSource.filter = peopleFilter(queryParams.search, null); - - this.providerId = urlParams.providerId; - const provider = await firstValueFrom( - this.accountService.activeAccount$.pipe( - getUserId, - switchMap((userId) => this.providerService.get$(this.providerId, userId)), - ), - ); - - if (!provider || !provider.canManageUsers) { - return await this.router.navigate(["../"], { relativeTo: this.activatedRoute }); - } - this.accessEvents = provider.useEvents; - await this.load(); - - if (queryParams.viewEvents != null) { - const user = this.dataSource.data.find((user) => user.id === queryParams.viewEvents); - if (user && user.status === ProviderUserStatusType.Confirmed) { - this.openEventsDialog(user); - } - } - }), - takeUntilDestroyed(), - ) - .subscribe(); - } - - async bulkConfirm(): Promise { - if (this.actionPromise != null) { - return; - } - - const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount); - - const dialogRef = BulkConfirmDialogComponent.open(this.dialogService, { - data: { - providerId: this.providerId, - users: users, - }, - }); - - await lastValueFrom(dialogRef.closed); - await this.load(); - } - - async bulkReinvite(): Promise { - if (this.actionPromise != null) { - return; - } - - let users: ProviderUser[]; - if (this.dataSource.isIncreasedBulkLimitEnabled()) { - users = this.dataSource.getCheckedUsersInVisibleOrder(); - } else { - users = this.dataSource.getCheckedUsers(); - } - - const allInvitedUsers = users.filter((user) => user.status === ProviderUserStatusType.Invited); - - // Capture the original count BEFORE enforcing the limit - const originalInvitedCount = allInvitedUsers.length; - - // When feature flag is enabled, limit invited users and uncheck the excess - let checkedInvitedUsers: ProviderUser[]; - if (this.dataSource.isIncreasedBulkLimitEnabled()) { - checkedInvitedUsers = this.dataSource.limitAndUncheckExcess( - allInvitedUsers, - CloudBulkReinviteLimit, - ); - } else { - checkedInvitedUsers = allInvitedUsers; - } - - if (checkedInvitedUsers.length <= 0) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("noSelectedUsersApplicable"), - }); - return; - } - - try { - // When feature flag is enabled, show toast instead of dialog - if (this.dataSource.isIncreasedBulkLimitEnabled()) { - await this.apiService.postManyProviderUserReinvite( - this.providerId, - new ProviderUserBulkRequest(checkedInvitedUsers.map((user) => user.id)), - ); - - const selectedCount = originalInvitedCount; - const invitedCount = checkedInvitedUsers.length; - - if (selectedCount > CloudBulkReinviteLimit) { - const excludedCount = selectedCount - CloudBulkReinviteLimit; - this.toastService.showToast({ - variant: "success", - message: this.i18nService.t( - "bulkReinviteLimitedSuccessToast", - CloudBulkReinviteLimit.toLocaleString(), - selectedCount.toLocaleString(), - excludedCount.toLocaleString(), - ), - }); - } else { - this.toastService.showToast({ - variant: "success", - message: - invitedCount === 1 - ? this.i18nService.t("reinviteSuccessToast") - : this.i18nService.t("bulkReinviteSentToast", invitedCount.toString()), - }); - } - } else { - // Feature flag disabled - show legacy dialog - const request = this.apiService.postManyProviderUserReinvite( - this.providerId, - new ProviderUserBulkRequest(checkedInvitedUsers.map((user) => user.id)), - ); - - const dialogRef = BulkStatusComponent.open(this.dialogService, { - data: { - users: users, - filteredUsers: checkedInvitedUsers, - request, - successfulMessage: this.i18nService.t("bulkReinviteMessage"), - }, - }); - await lastValueFrom(dialogRef.closed); - } - } catch (error) { - this.validationService.showError(error); - } - } - - async invite() { - await this.edit(null); - } - - async bulkRemove(): Promise { - if (this.actionPromise != null) { - return; - } - - const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount); - - const dialogRef = BulkRemoveDialogComponent.open(this.dialogService, { - data: { - providerId: this.providerId, - users: users, - }, - }); - - await lastValueFrom(dialogRef.closed); - await this.load(); - } - - async confirmUser(user: ProviderUser, publicKey: Uint8Array): Promise { - try { - const providerKey = await firstValueFrom( - this.accountService.activeAccount$.pipe( - getUserId, - switchMap((userId) => this.keyService.providerKeys$(userId)), - map((providerKeys) => providerKeys?.[this.providerId as ProviderId] ?? null), - ), - ); - assertNonNullish(providerKey, "Provider key not found"); - - const key = await this.encryptService.encapsulateKeyUnsigned(providerKey, publicKey); - const request = new ProviderUserConfirmRequest(key.encryptedString); - await this.apiService.postProviderUserConfirm(this.providerId, user.id, request); - return { success: true }; - } catch (error) { - return { success: false, error: error.message }; - } - } - - removeUser = async (id: string): Promise => { - try { - await this.apiService.deleteProviderUser(this.providerId, id); - return { success: true }; - } catch (error) { - return { success: false, error: error.message }; - } - }; - - edit = async (user: ProviderUser | null): Promise => { - const data: AddEditMemberDialogParams = { - providerId: this.providerId, - user, - }; - - const dialogRef = AddEditMemberDialogComponent.open(this.dialogService, { - data, - }); - - const result = await lastValueFrom(dialogRef.closed); - - switch (result) { - case AddEditMemberDialogResultType.Saved: - case AddEditMemberDialogResultType.Deleted: - await this.load(); - break; - } - }; - - openEventsDialog = (user: ProviderUser): DialogRef => - openEntityEventsDialog(this.dialogService, { - data: { - name: this.userNamePipe.transform(user), - providerId: this.providerId, - entityId: user.id, - showUser: false, - entity: "user", - }, - }); - - getUsers = (): Promise> => - this.apiService.getProviderUsers(this.providerId); - - reinviteUser = async (id: string): Promise => { - try { - await this.apiService.postProviderUserReinvite(this.providerId, id); - return { success: true }; - } catch (error) { - return { success: false, error: error.message }; - } - }; - - get selectedInvitedCount(): number { - return this.dataSource - .getCheckedUsers() - .filter((member) => member.status === this.userStatusType.Invited).length; - } - - get isSingleInvite(): boolean { - return this.selectedInvitedCount === 1; - } -} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts index c63bda449c5..d4a6ba92451 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts @@ -61,7 +61,7 @@ interface BulkProviderFlags { templateUrl: "members.component.html", standalone: false, }) -export class vNextMembersComponent { +export class MembersComponent { protected apiService = inject(ApiService); protected dialogService = inject(DialogService); protected i18nService = inject(I18nService); @@ -236,10 +236,12 @@ export class vNextMembersComponent { } } else { // In self-hosted environments, show legacy dialog - const request = this.apiService.postManyProviderUserReinvite( - providerId, - new ProviderUserBulkRequest(checkedInvitedUsers.map((user) => user.id)), - ); + const request = this.apiService + .postManyProviderUserReinvite( + providerId, + new ProviderUserBulkRequest(checkedInvitedUsers.map((user) => user.id)), + ) + .then((response) => response.data); const dialogRef = BulkStatusComponent.open(this.dialogService, { data: { diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts index 447481a8bcb..5fadc935644 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts @@ -2,9 +2,7 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; import { authGuard } from "@bitwarden/angular/auth/guards"; -import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { AnonLayoutWrapperComponent } from "@bitwarden/components"; import { FrontendLayoutComponent } from "@bitwarden/web-vault/app/layouts/frontend-layout.component"; import { UserLayoutComponent } from "@bitwarden/web-vault/app/layouts/user-layout.component"; @@ -17,9 +15,8 @@ import { ProviderSubscriptionComponent } from "../../billing/providers/subscript import { ManageClientsComponent } from "./clients/manage-clients.component"; import { providerPermissionsGuard } from "./guards/provider-permissions.guard"; import { AcceptProviderComponent } from "./manage/accept-provider.component"; -import { MembersComponent } from "./manage/deprecated_members.component"; import { EventsComponent } from "./manage/events.component"; -import { vNextMembersComponent } from "./manage/members.component"; +import { MembersComponent } from "./manage/members.component"; import { ProvidersLayoutComponent } from "./providers-layout.component"; import { ProvidersComponent } from "./providers.component"; import { AccountComponent } from "./settings/account.component"; @@ -95,20 +92,16 @@ const routes: Routes = [ pathMatch: "full", redirectTo: "members", }, - ...featureFlaggedRoute({ - defaultComponent: MembersComponent, - flaggedComponent: vNextMembersComponent, - featureFlag: FeatureFlag.MembersComponentRefactor, - routeOptions: { - path: "members", - canActivate: [ - providerPermissionsGuard((provider: Provider) => provider.canManageUsers), - ], - data: { - titleId: "members", - }, + { + path: "members", + component: MembersComponent, + canActivate: [ + providerPermissionsGuard((provider: Provider) => provider.canManageUsers), + ], + data: { + titleId: "members", }, - }), + }, { path: "events", component: EventsComponent, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index 44e2e51637f..abdd35c5e61 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -27,12 +27,11 @@ import { CreateClientDialogComponent } from "./clients/create-client-dialog.comp import { ManageClientNameDialogComponent } from "./clients/manage-client-name-dialog.component"; import { ManageClientSubscriptionDialogComponent } from "./clients/manage-client-subscription-dialog.component"; import { AcceptProviderComponent } from "./manage/accept-provider.component"; -import { MembersComponent } from "./manage/deprecated_members.component"; import { AddEditMemberDialogComponent } from "./manage/dialogs/add-edit-member-dialog.component"; import { BulkConfirmDialogComponent } from "./manage/dialogs/bulk-confirm-dialog.component"; import { BulkRemoveDialogComponent } from "./manage/dialogs/bulk-remove-dialog.component"; import { EventsComponent } from "./manage/events.component"; -import { vNextMembersComponent } from "./manage/members.component"; +import { MembersComponent } from "./manage/members.component"; import { ProviderActionsService } from "./manage/services/provider-actions/provider-actions.service"; import { ProvidersLayoutComponent } from "./providers-layout.component"; import { ProvidersRoutingModule } from "./providers-routing.module"; @@ -67,7 +66,6 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr BulkConfirmDialogComponent, BulkRemoveDialogComponent, EventsComponent, - vNextMembersComponent, MembersComponent, SetupComponent, SetupProviderComponent, diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.ts index 65a31896341..2307eab04fe 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.ts @@ -1,8 +1,10 @@ import { BehaviorSubject, combineLatest, Observable } from "rxjs"; import { map, shareReplay } from "rxjs/operators"; -import { RiskInsightsDataService } from "@bitwarden/bit-common/dirt/reports/risk-insights"; -import { SecurityTasksApiService } from "@bitwarden/bit-common/dirt/reports/risk-insights"; +import { + RiskInsightsDataService, + SecurityTasksApiService, +} from "@bitwarden/bit-common/dirt/reports/risk-insights"; import { CipherId, OrganizationId } from "@bitwarden/common/types/guid"; import { SecurityTask, SecurityTaskStatus, SecurityTaskType } from "@bitwarden/common/vault/tasks"; diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 0f857e67247..2fbf55bf6c5 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -56,6 +56,7 @@ import { UserDecryptionOptionsService, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; @@ -1079,6 +1080,7 @@ const safeProviders: SafeProvider[] = [ AuthRequestAnsweringService, ConfigService, InternalPolicyService, + AutomaticUserConfirmationService, ], }), safeProvider({ diff --git a/libs/auto-confirm/src/abstractions/auto-confirm.service.abstraction.ts b/libs/auto-confirm/src/abstractions/auto-confirm.service.abstraction.ts index 9ce6cb9c1a4..1ef3be4ff4e 100644 --- a/libs/auto-confirm/src/abstractions/auto-confirm.service.abstraction.ts +++ b/libs/auto-confirm/src/abstractions/auto-confirm.service.abstraction.ts @@ -1,6 +1,6 @@ import { Observable } from "rxjs"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/user-core"; import { AutoConfirmState } from "../models/auto-confirm-state.model"; @@ -27,12 +27,12 @@ export abstract class AutomaticUserConfirmationService { /** * Calls the API endpoint to initiate automatic user confirmation. * @param userId The userId of the logged in admin performing auto confirmation. This is neccesary to perform the key exchange and for permissions checks. - * @param confirmingUserId The userId of the user being confirmed. - * @param organization the organization the user is being auto confirmed to. + * @param confirmedUserId The userId of the member being confirmed. + * @param organization the organization the member is being auto confirmed to. **/ abstract autoConfirmUser( userId: UserId, - confirmingUserId: UserId, - organization: Organization, + confirmedUserId: UserId, + organization: OrganizationId, ): Promise; } diff --git a/libs/auto-confirm/src/components/auto-confirm-extension-dialog.component.ts b/libs/auto-confirm/src/angular/components/auto-confirm-extension-dialog.component.ts similarity index 100% rename from libs/auto-confirm/src/components/auto-confirm-extension-dialog.component.ts rename to libs/auto-confirm/src/angular/components/auto-confirm-extension-dialog.component.ts diff --git a/libs/auto-confirm/src/components/auto-confirm-warning-dialog.component.html b/libs/auto-confirm/src/angular/components/auto-confirm-warning-dialog.component.html similarity index 100% rename from libs/auto-confirm/src/components/auto-confirm-warning-dialog.component.html rename to libs/auto-confirm/src/angular/components/auto-confirm-warning-dialog.component.html diff --git a/libs/auto-confirm/src/components/auto-confirm-warning-dialog.component.ts b/libs/auto-confirm/src/angular/components/auto-confirm-warning-dialog.component.ts similarity index 100% rename from libs/auto-confirm/src/components/auto-confirm-warning-dialog.component.ts rename to libs/auto-confirm/src/angular/components/auto-confirm-warning-dialog.component.ts diff --git a/libs/auto-confirm/src/components/index.ts b/libs/auto-confirm/src/angular/components/index.ts similarity index 100% rename from libs/auto-confirm/src/components/index.ts rename to libs/auto-confirm/src/angular/components/index.ts diff --git a/libs/auto-confirm/src/guards/automatic-user-confirmation-settings.guard.spec.ts b/libs/auto-confirm/src/angular/guards/automatic-user-confirmation-settings.guard.spec.ts similarity index 97% rename from libs/auto-confirm/src/guards/automatic-user-confirmation-settings.guard.spec.ts rename to libs/auto-confirm/src/angular/guards/automatic-user-confirmation-settings.guard.spec.ts index aca51edb8dc..0261a1a86dc 100644 --- a/libs/auto-confirm/src/guards/automatic-user-confirmation-settings.guard.spec.ts +++ b/libs/auto-confirm/src/angular/guards/automatic-user-confirmation-settings.guard.spec.ts @@ -3,14 +3,13 @@ import { Router, UrlTree } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom, Observable, of } from "rxjs"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { UserId } from "@bitwarden/common/types/guid"; import { ToastService } from "@bitwarden/components"; import { newGuid } from "@bitwarden/guid"; -import { AutomaticUserConfirmationService } from "../abstractions"; - import { canAccessAutoConfirmSettings } from "./automatic-user-confirmation-settings.guard"; describe("canAccessAutoConfirmSettings", () => { diff --git a/libs/auto-confirm/src/guards/automatic-user-confirmation-settings.guard.ts b/libs/auto-confirm/src/angular/guards/automatic-user-confirmation-settings.guard.ts similarity index 94% rename from libs/auto-confirm/src/guards/automatic-user-confirmation-settings.guard.ts rename to libs/auto-confirm/src/angular/guards/automatic-user-confirmation-settings.guard.ts index 77f01ba2801..3ae6b5b4c52 100644 --- a/libs/auto-confirm/src/guards/automatic-user-confirmation-settings.guard.ts +++ b/libs/auto-confirm/src/angular/guards/automatic-user-confirmation-settings.guard.ts @@ -2,13 +2,12 @@ import { inject } from "@angular/core"; import { CanActivateFn, Router } from "@angular/router"; import { map, switchMap } from "rxjs"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities"; import { ToastService } from "@bitwarden/components"; -import { AutomaticUserConfirmationService } from "../abstractions"; - export const canAccessAutoConfirmSettings: CanActivateFn = () => { const accountService = inject(AccountService); const autoConfirmService = inject(AutomaticUserConfirmationService); diff --git a/libs/auto-confirm/src/guards/index.ts b/libs/auto-confirm/src/angular/guards/index.ts similarity index 100% rename from libs/auto-confirm/src/guards/index.ts rename to libs/auto-confirm/src/angular/guards/index.ts diff --git a/libs/auto-confirm/src/angular/index.ts b/libs/auto-confirm/src/angular/index.ts new file mode 100644 index 00000000000..ff2d69248b4 --- /dev/null +++ b/libs/auto-confirm/src/angular/index.ts @@ -0,0 +1,8 @@ +// Re-export core auto-confirm functionality for convenience +export * from "../abstractions"; +export * from "../models"; +export * from "../services"; + +// Angular-specific exports +export * from "./components"; +export * from "./guards"; diff --git a/libs/auto-confirm/src/index.ts b/libs/auto-confirm/src/index.ts index 56b9d0b0285..9187ccd39cf 100644 --- a/libs/auto-confirm/src/index.ts +++ b/libs/auto-confirm/src/index.ts @@ -1,5 +1,3 @@ export * from "./abstractions"; -export * from "./components"; -export * from "./guards"; export * from "./models"; export * from "./services"; diff --git a/libs/auto-confirm/src/services/default-auto-confirm.service.spec.ts b/libs/auto-confirm/src/services/default-auto-confirm.service.spec.ts index 1d37378b96c..0ea3ca9c23a 100644 --- a/libs/auto-confirm/src/services/default-auto-confirm.service.spec.ts +++ b/libs/auto-confirm/src/services/default-auto-confirm.service.spec.ts @@ -377,48 +377,70 @@ describe("DefaultAutomaticUserConfirmationService", () => { defaultUserCollectionName: "encrypted-collection", } as OrganizationUserConfirmRequest; - beforeEach(() => { + beforeEach(async () => { const organizations$ = new BehaviorSubject([mockOrganization]); organizationService.organizations$.mockReturnValue(organizations$); configService.getFeatureFlag$.mockReturnValue(of(true)); policyService.policyAppliesToUser$.mockReturnValue(of(true)); + // Enable auto-confirm configuration for the user + const enabledConfig = new AutoConfirmState(); + enabledConfig.enabled = true; + await stateProvider.setUserState( + AUTO_CONFIRM_STATE, + { [mockUserId]: enabledConfig }, + mockUserId, + ); + apiService.getUserPublicKey.mockResolvedValue({ publicKey: mockPublicKey, } as UserKeyResponse); jest.spyOn(Utils, "fromB64ToArray").mockReturnValue(mockPublicKeyArray); organizationUserService.buildConfirmRequest.mockReturnValue(of(mockConfirmRequest)); - organizationUserApiService.postOrganizationUserConfirm.mockResolvedValue(undefined); + organizationUserApiService.postOrganizationUserAutoConfirm.mockResolvedValue(undefined); }); - it("should successfully auto-confirm a user", async () => { - await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization); + it("should successfully auto-confirm a user with organizationId", async () => { + await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId); expect(apiService.getUserPublicKey).toHaveBeenCalledWith(mockUserId); expect(organizationUserService.buildConfirmRequest).toHaveBeenCalledWith( mockOrganization, mockPublicKeyArray, ); - expect(organizationUserApiService.postOrganizationUserConfirm).toHaveBeenCalledWith( + expect(organizationUserApiService.postOrganizationUserAutoConfirm).toHaveBeenCalledWith( mockOrganizationId, mockConfirmingUserId, mockConfirmRequest, ); }); - it("should not confirm user when canManageAutoConfirm returns false", async () => { + it("should return early when canManageAutoConfirm returns false", async () => { configService.getFeatureFlag$.mockReturnValue(of(false)); - await expect( - service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization), - ).rejects.toThrow("Cannot automatically confirm user (insufficient permissions)"); + await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId); expect(apiService.getUserPublicKey).not.toHaveBeenCalled(); - expect(organizationUserApiService.postOrganizationUserConfirm).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAutoConfirm).not.toHaveBeenCalled(); + }); + + it("should return early when auto-confirm is disabled in configuration", async () => { + const disabledConfig = new AutoConfirmState(); + disabledConfig.enabled = false; + await stateProvider.setUserState( + AUTO_CONFIRM_STATE, + { [mockUserId]: disabledConfig }, + mockUserId, + ); + + await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId); + + expect(apiService.getUserPublicKey).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAutoConfirm).not.toHaveBeenCalled(); }); it("should build confirm request with organization and public key", async () => { - await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization); + await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId); expect(organizationUserService.buildConfirmRequest).toHaveBeenCalledWith( mockOrganization, @@ -427,10 +449,10 @@ describe("DefaultAutomaticUserConfirmationService", () => { }); it("should call API with correct parameters", async () => { - await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization); + await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId); - expect(organizationUserApiService.postOrganizationUserConfirm).toHaveBeenCalledWith( - mockOrganization.id, + expect(organizationUserApiService.postOrganizationUserAutoConfirm).toHaveBeenCalledWith( + mockOrganizationId, mockConfirmingUserId, mockConfirmRequest, ); @@ -441,10 +463,10 @@ describe("DefaultAutomaticUserConfirmationService", () => { apiService.getUserPublicKey.mockRejectedValue(apiError); await expect( - service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization), + service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId), ).rejects.toThrow("API Error"); - expect(organizationUserApiService.postOrganizationUserConfirm).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAutoConfirm).not.toHaveBeenCalled(); }); it("should handle buildConfirmRequest errors gracefully", async () => { @@ -452,10 +474,10 @@ describe("DefaultAutomaticUserConfirmationService", () => { organizationUserService.buildConfirmRequest.mockReturnValue(throwError(() => buildError)); await expect( - service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization), + service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId), ).rejects.toThrow("Build Error"); - expect(organizationUserApiService.postOrganizationUserConfirm).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAutoConfirm).not.toHaveBeenCalled(); }); }); }); diff --git a/libs/auto-confirm/src/services/default-auto-confirm.service.ts b/libs/auto-confirm/src/services/default-auto-confirm.service.ts index 109ccb6c9db..821340a0a9c 100644 --- a/libs/auto-confirm/src/services/default-auto-confirm.service.ts +++ b/libs/auto-confirm/src/services/default-auto-confirm.service.ts @@ -8,10 +8,11 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; 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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { getById } from "@bitwarden/common/platform/misc"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { StateProvider } from "@bitwarden/state"; import { UserId } from "@bitwarden/user-core"; @@ -66,26 +67,44 @@ export class DefaultAutomaticUserConfirmationService implements AutomaticUserCon async autoConfirmUser( userId: UserId, - confirmingUserId: UserId, - organization: Organization, + confirmedUserId: UserId, + organizationId: OrganizationId, ): Promise { + const canManage = await firstValueFrom(this.canManageAutoConfirm$(userId)); + + if (!canManage) { + return; + } + + // Only initiate auto confirmation if the local client setting has been turned on + const autoConfirmEnabled = await firstValueFrom( + this.configuration$(userId).pipe(map((state) => state.enabled)), + ); + + if (!autoConfirmEnabled) { + return; + } + + const organization$ = this.organizationService.organizations$(userId).pipe( + getById(organizationId), + map((organization) => { + if (organization == null) { + throw new Error("Organization not found"); + } + return organization; + }), + ); + + const publicKeyResponse = await this.apiService.getUserPublicKey(userId); + const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey); + await firstValueFrom( - this.canManageAutoConfirm$(userId).pipe( - map((canManage) => { - if (!canManage) { - throw new Error("Cannot automatically confirm user (insufficient permissions)"); - } - return canManage; - }), - switchMap(() => this.apiService.getUserPublicKey(userId)), - map((publicKeyResponse) => Utils.fromB64ToArray(publicKeyResponse.publicKey)), - switchMap((publicKey) => - this.organizationUserService.buildConfirmRequest(organization, publicKey), - ), + organization$.pipe( + switchMap((org) => this.organizationUserService.buildConfirmRequest(org, publicKey)), switchMap((request) => - this.organizationUserApiService.postOrganizationUserConfirm( - organization.id, - confirmingUserId, + this.organizationUserApiService.postOrganizationUserAutoConfirm( + organizationId, + confirmedUserId, request, ), ), diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 5298ed34eda..96196fbc8e1 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -13,7 +13,6 @@ export enum FeatureFlag { /* Admin Console Team */ AutoConfirm = "pm-19934-auto-confirm-organization-users", DefaultUserCollectionRestore = "pm-30883-my-items-restored-users", - MembersComponentRefactor = "pm-29503-refactor-members-inheritance", BulkReinviteUI = "pm-28416-bulk-reinvite-ux-improvements", /* Auth */ @@ -110,7 +109,6 @@ export const DefaultFeatureFlagValue = { /* Admin Console Team */ [FeatureFlag.AutoConfirm]: FALSE, [FeatureFlag.DefaultUserCollectionRestore]: FALSE, - [FeatureFlag.MembersComponentRefactor]: FALSE, [FeatureFlag.BulkReinviteUI]: FALSE, /* Autofill */ diff --git a/libs/common/src/enums/notification-type.enum.ts b/libs/common/src/enums/notification-type.enum.ts index a10e6bf4448..d323dda4d74 100644 --- a/libs/common/src/enums/notification-type.enum.ts +++ b/libs/common/src/enums/notification-type.enum.ts @@ -35,4 +35,5 @@ export enum NotificationType { ProviderBankAccountVerified = 24, SyncPolicy = 25, + AutoConfirmMember = 26, } diff --git a/libs/common/src/models/response/notification.response.spec.ts b/libs/common/src/models/response/notification.response.spec.ts new file mode 100644 index 00000000000..91a1390bfdb --- /dev/null +++ b/libs/common/src/models/response/notification.response.spec.ts @@ -0,0 +1,63 @@ +import { NotificationType } from "../../enums"; + +import { AutoConfirmMemberNotification, NotificationResponse } from "./notification.response"; + +describe("NotificationResponse", () => { + describe("AutoConfirmMemberNotification", () => { + it("should parse AutoConfirmMemberNotification payload", () => { + const response = { + ContextId: "context-123", + Type: NotificationType.AutoConfirmMember, + Payload: { + TargetUserId: "target-user-id", + UserId: "user-id", + OrganizationId: "org-id", + }, + }; + + const notification = new NotificationResponse(response); + + expect(notification.type).toBe(NotificationType.AutoConfirmMember); + expect(notification.payload).toBeInstanceOf(AutoConfirmMemberNotification); + expect(notification.payload.targetUserId).toBe("target-user-id"); + expect(notification.payload.userId).toBe("user-id"); + expect(notification.payload.organizationId).toBe("org-id"); + }); + + it("should handle stringified JSON payload", () => { + const response = { + ContextId: "context-123", + Type: NotificationType.AutoConfirmMember, + Payload: JSON.stringify({ + TargetUserId: "target-user-id-2", + UserId: "user-id-2", + OrganizationId: "org-id-2", + }), + }; + + const notification = new NotificationResponse(response); + + expect(notification.type).toBe(NotificationType.AutoConfirmMember); + expect(notification.payload).toBeInstanceOf(AutoConfirmMemberNotification); + expect(notification.payload.targetUserId).toBe("target-user-id-2"); + expect(notification.payload.userId).toBe("user-id-2"); + expect(notification.payload.organizationId).toBe("org-id-2"); + }); + }); + + describe("AutoConfirmMemberNotification constructor", () => { + it("should extract all properties from response", () => { + const response = { + TargetUserId: "target-user-id", + UserId: "user-id", + OrganizationId: "org-id", + }; + + const notification = new AutoConfirmMemberNotification(response); + + expect(notification.targetUserId).toBe("target-user-id"); + expect(notification.userId).toBe("user-id"); + expect(notification.organizationId).toBe("org-id"); + }); + }); +}); diff --git a/libs/common/src/models/response/notification.response.ts b/libs/common/src/models/response/notification.response.ts index 2c0c0aae3f1..27232696d2e 100644 --- a/libs/common/src/models/response/notification.response.ts +++ b/libs/common/src/models/response/notification.response.ts @@ -75,6 +75,9 @@ export class NotificationResponse extends BaseResponse { case NotificationType.SyncPolicy: this.payload = new SyncPolicyNotification(payload); break; + case NotificationType.AutoConfirmMember: + this.payload = new AutoConfirmMemberNotification(payload); + break; default: break; } @@ -210,3 +213,16 @@ export class LogOutNotification extends BaseResponse { this.reason = this.getResponseProperty("Reason"); } } + +export class AutoConfirmMemberNotification extends BaseResponse { + userId: string; + targetUserId: string; + organizationId: string; + + constructor(response: any) { + super(response); + this.targetUserId = this.getResponseProperty("TargetUserId"); + this.userId = this.getResponseProperty("UserId"); + this.organizationId = this.getResponseProperty("OrganizationId"); + } +} diff --git a/libs/common/src/platform/server-notifications/internal/default-server-notifications.multiuser.spec.ts b/libs/common/src/platform/server-notifications/internal/default-server-notifications.multiuser.spec.ts index 2795e4c3003..70b93c77f1c 100644 --- a/libs/common/src/platform/server-notifications/internal/default-server-notifications.multiuser.spec.ts +++ b/libs/common/src/platform/server-notifications/internal/default-server-notifications.multiuser.spec.ts @@ -3,6 +3,7 @@ import { BehaviorSubject, bufferCount, firstValueFrom, Subject, ObservedValueOf // eslint-disable-next-line no-restricted-imports import { LogoutReason } from "@bitwarden/auth/common"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; @@ -36,6 +37,7 @@ describe("DefaultServerNotificationsService (multi-user)", () => { let authRequestAnsweringService: MockProxy; let configService: MockProxy; let policyService: MockProxy; + let autoConfirmService: MockProxy; let activeUserAccount$: BehaviorSubject>; let userAccounts$: BehaviorSubject>; @@ -131,6 +133,8 @@ describe("DefaultServerNotificationsService (multi-user)", () => { policyService = mock(); + autoConfirmService = mock(); + defaultServerNotificationsService = new DefaultServerNotificationsService( mock(), syncService, @@ -145,6 +149,7 @@ describe("DefaultServerNotificationsService (multi-user)", () => { authRequestAnsweringService, configService, policyService, + autoConfirmService, ); }); diff --git a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.spec.ts b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.spec.ts index f058e8794ac..a54509925ef 100644 --- a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.spec.ts +++ b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.spec.ts @@ -4,6 +4,7 @@ import { BehaviorSubject, bufferCount, firstValueFrom, ObservedValueOf, of, Subj // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { LogoutReason } from "@bitwarden/auth/common"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; @@ -45,6 +46,7 @@ describe("NotificationsService", () => { let authRequestAnsweringService: MockProxy; let configService: MockProxy; let policyService: MockProxy; + let autoConfirmService: MockProxy; let activeAccount: BehaviorSubject>; let accounts: BehaviorSubject>; @@ -75,6 +77,7 @@ describe("NotificationsService", () => { authRequestAnsweringService = mock(); configService = mock(); policyService = mock(); + autoConfirmService = mock(); // For these tests, use the active-user implementation (feature flag disabled) configService.getFeatureFlag$.mockImplementation(() => of(true)); @@ -128,6 +131,7 @@ describe("NotificationsService", () => { authRequestAnsweringService, configService, policyService, + autoConfirmService, ); }); @@ -507,5 +511,29 @@ describe("NotificationsService", () => { }); }); }); + + describe("NotificationType.AutoConfirmMember", () => { + it("should call autoConfirmService.autoConfirmUser with correct parameters", async () => { + autoConfirmService.autoConfirmUser.mockResolvedValue(); + + const notification = new NotificationResponse({ + type: NotificationType.AutoConfirmMember, + payload: { + UserId: mockUser1, + TargetUserId: "target-user-id", + OrganizationId: "org-id", + }, + contextId: "different-app-id", + }); + + await sut["processNotification"](notification, mockUser1); + + expect(autoConfirmService.autoConfirmUser).toHaveBeenCalledWith( + mockUser1, + "target-user-id", + "org-id", + ); + }); + }); }); }); diff --git a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts index 83ea12bf154..1a43c0edb09 100644 --- a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts +++ b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts @@ -15,6 +15,7 @@ import { // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { LogoutReason } from "@bitwarden/auth/common"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data"; import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; @@ -49,6 +50,7 @@ export const DISABLED_NOTIFICATIONS_URL = "http://-"; export const AllowedMultiUserNotificationTypes = new Set([ NotificationType.AuthRequest, + NotificationType.AutoConfirmMember, ]); export class DefaultServerNotificationsService implements ServerNotificationsService { @@ -70,6 +72,7 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer private readonly authRequestAnsweringService: AuthRequestAnsweringService, private readonly configService: ConfigService, private readonly policyService: InternalPolicyService, + private autoConfirmService: AutomaticUserConfirmationService, ) { this.notifications$ = this.accountService.accounts$.pipe( map((accounts: Record): Set => { @@ -292,6 +295,13 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer case NotificationType.SyncPolicy: await this.policyService.syncPolicy(PolicyData.fromPolicy(notification.payload.policy)); break; + case NotificationType.AutoConfirmMember: + await this.autoConfirmService.autoConfirmUser( + notification.payload.userId, + notification.payload.targetUserId, + notification.payload.organizationId, + ); + break; default: break; } diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts index 68c03503e8d..a25b1b3c210 100644 --- a/libs/common/src/platform/sync/default-sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -183,6 +183,8 @@ export class DefaultSyncService extends CoreSyncService { const response = await this.inFlightApiCalls.sync; + await this.cipherService.clear(response.profile.id); + await this.syncUserDecryption(response.profile.id, response.userDecryption); await this.syncProfile(response.profile); await this.syncFolders(response.folders, response.profile.id); diff --git a/libs/common/src/tools/send/services/send-api.service.ts b/libs/common/src/tools/send/services/send-api.service.ts index 57004b6ff0e..fdbbd579c3f 100644 --- a/libs/common/src/tools/send/services/send-api.service.ts +++ b/libs/common/src/tools/send/services/send-api.service.ts @@ -105,7 +105,7 @@ export class SendApiService implements SendApiServiceAbstraction { "POST", "/sends/access/file/" + send.file.id, null, - true, + false, true, apiUrl, setAuthTokenHeader, diff --git a/libs/common/src/vault/abstractions/search.service.ts b/libs/common/src/vault/abstractions/search.service.ts index 29575ec3af9..b4dfc015efe 100644 --- a/libs/common/src/vault/abstractions/search.service.ts +++ b/libs/common/src/vault/abstractions/search.service.ts @@ -2,7 +2,6 @@ import { Observable } from "rxjs"; import { SendView } from "../../tools/send/models/view/send.view"; import { IndexedEntityId, UserId } from "../../types/guid"; -import { CipherView } from "../models/view/cipher.view"; import { CipherViewLike } from "../utils/cipher-view-like-utils"; export abstract class SearchService { @@ -20,7 +19,7 @@ export abstract class SearchService { abstract isSearchable(userId: UserId, query: string | null): Promise; abstract indexCiphers( userId: UserId, - ciphersToIndex: CipherView[], + ciphersToIndex: CipherViewLike[], indexedEntityGuid?: string, ): Promise; abstract searchCiphers( diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 06c6628f158..e4c4f892b4a 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -173,13 +173,14 @@ export class CipherService implements CipherServiceAbstraction { decryptStartTime = performance.now(); }), switchMap(async (ciphers) => { - const [decrypted, failures] = await this.decryptCiphersWithSdk(ciphers, userId, false); - void this.setFailedDecryptedCiphers(failures, userId); - // Trigger full decryption and indexing in background - void this.getAllDecrypted(userId); - return decrypted; + return await this.decryptCiphersWithSdk(ciphers, userId, false); }), - tap((decrypted) => { + tap(([decrypted, failures]) => { + void Promise.all([ + this.setFailedDecryptedCiphers(failures, userId), + this.searchService.indexCiphers(userId, decrypted), + ]); + this.logService.measure( decryptStartTime, "Vault", @@ -188,10 +189,11 @@ export class CipherService implements CipherServiceAbstraction { [["Items", decrypted.length]], ); }), + map(([decrypted]) => decrypted), ); }), ); - }); + }, this.clearCipherViewsForUser$); /** * Observable that emits an array of decrypted ciphers for the active user. @@ -530,6 +532,10 @@ export class CipherService implements CipherServiceAbstraction { ciphers: Cipher[], userId: UserId, ): Promise<[CipherView[], CipherView[]] | null> { + if (ciphers.length === 0) { + return [[], []]; + } + if (await this.configService.getFeatureFlag(FeatureFlag.PM19941MigrateCipherDomainToSdk)) { const decryptStartTime = performance.now(); @@ -2401,6 +2407,12 @@ export class CipherService implements CipherServiceAbstraction { userId: UserId, fullDecryption: boolean = true, ): Promise<[CipherViewLike[], CipherView[]]> { + // Short-circuit if there are no ciphers to decrypt + // Observables reacting to key changes may attempt to decrypt with a stale SDK reference. + if (ciphers.length === 0) { + return [[], []]; + } + if (fullDecryption) { const [decryptedViews, failedViews] = await this.cipherEncryptionService.decryptManyLegacy( ciphers, diff --git a/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts b/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts index a0ca4833b92..98b554b5762 100644 --- a/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts +++ b/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts @@ -95,6 +95,7 @@ describe("DefaultCipherEncryptionService", () => { vault: jest.fn().mockReturnValue({ ciphers: jest.fn().mockReturnValue({ encrypt: jest.fn(), + encrypt_list: jest.fn(), encrypt_cipher_for_rotation: jest.fn(), set_fido2_credentials: jest.fn(), decrypt: jest.fn(), @@ -280,10 +281,23 @@ describe("DefaultCipherEncryptionService", () => { name: "encrypted-name-3", } as unknown as Cipher; - mockSdkClient.vault().ciphers().encrypt.mockReturnValue({ - cipher: sdkCipher, - encryptedFor: userId, - }); + mockSdkClient + .vault() + .ciphers() + .encrypt_list.mockReturnValue([ + { + cipher: sdkCipher, + encryptedFor: userId, + }, + { + cipher: sdkCipher, + encryptedFor: userId, + }, + { + cipher: sdkCipher, + encryptedFor: userId, + }, + ]); jest .spyOn(Cipher, "fromSdkCipher") @@ -299,7 +313,8 @@ describe("DefaultCipherEncryptionService", () => { expect(results[1].cipher).toEqual(expectedCipher2); expect(results[2].cipher).toEqual(expectedCipher3); - expect(mockSdkClient.vault().ciphers().encrypt).toHaveBeenCalledTimes(3); + expect(mockSdkClient.vault().ciphers().encrypt_list).toHaveBeenCalledTimes(1); + expect(mockSdkClient.vault().ciphers().encrypt).not.toHaveBeenCalled(); expect(results[0].encryptedFor).toBe(userId); expect(results[1].encryptedFor).toBe(userId); @@ -311,7 +326,7 @@ describe("DefaultCipherEncryptionService", () => { expect(results).toBeDefined(); expect(results.length).toBe(0); - expect(mockSdkClient.vault().ciphers().encrypt).not.toHaveBeenCalled(); + expect(mockSdkClient.vault().ciphers().encrypt_list).not.toHaveBeenCalled(); }); }); diff --git a/libs/common/src/vault/services/default-cipher-encryption.service.ts b/libs/common/src/vault/services/default-cipher-encryption.service.ts index 588265846e0..45542091618 100644 --- a/libs/common/src/vault/services/default-cipher-encryption.service.ts +++ b/libs/common/src/vault/services/default-cipher-encryption.service.ts @@ -65,21 +65,14 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService { using ref = sdk.take(); - const results: EncryptionContext[] = []; - - // TODO: https://bitwarden.atlassian.net/browse/PM-30580 - // Replace this loop with a native SDK encryptMany method for better performance. - for (const model of models) { - const sdkCipherView = this.toSdkCipherView(model, ref.value); - const encryptionContext = ref.value.vault().ciphers().encrypt(sdkCipherView); - - results.push({ + return ref.value + .vault() + .ciphers() + .encrypt_list(models.map((model) => this.toSdkCipherView(model, ref.value))) + .map((encryptionContext) => ({ cipher: Cipher.fromSdkCipher(encryptionContext.cipher)!, encryptedFor: uuidAsString(encryptionContext.encryptedFor) as UserId, - }); - } - - return results; + })); }), catchError((error: unknown) => { this.logService.error(`Failed to encrypt ciphers in batch: ${error}`); diff --git a/libs/common/src/vault/services/search.service.ts b/libs/common/src/vault/services/search.service.ts index feb6a7494b5..e14a66aad6f 100644 --- a/libs/common/src/vault/services/search.service.ts +++ b/libs/common/src/vault/services/search.service.ts @@ -21,7 +21,6 @@ import { IndexedEntityId, UserId } from "../../types/guid"; import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service"; import { FieldType } from "../enums"; import { CipherType } from "../enums/cipher-type"; -import { CipherView } from "../models/view/cipher.view"; import { CipherViewLike, CipherViewLikeUtils } from "../utils/cipher-view-like-utils"; // Time to wait before performing a search after the user stops typing. @@ -169,7 +168,7 @@ export class SearchService implements SearchServiceAbstraction { async indexCiphers( userId: UserId, - ciphers: CipherView[], + ciphers: CipherViewLike[], indexedEntityId?: string, ): Promise { if (await this.getIsIndexing(userId)) { @@ -182,34 +181,47 @@ export class SearchService implements SearchServiceAbstraction { const builder = new lunr.Builder(); builder.pipeline.add(this.normalizeAccentsPipelineFunction); builder.ref("id"); - builder.field("shortid", { boost: 100, extractor: (c: CipherView) => c.id.substr(0, 8) }); + builder.field("shortid", { + boost: 100, + extractor: (c: CipherViewLike) => uuidAsString(c.id).substr(0, 8), + }); builder.field("name", { boost: 10, }); builder.field("subtitle", { boost: 5, - extractor: (c: CipherView) => { - if (c.subTitle != null && c.type === CipherType.Card) { - return c.subTitle.replace(/\*/g, ""); + extractor: (c: CipherViewLike) => { + const subtitle = CipherViewLikeUtils.subtitle(c); + if (subtitle != null && CipherViewLikeUtils.getType(c) === CipherType.Card) { + return subtitle.replace(/\*/g, ""); } - return c.subTitle; + return subtitle; }, }); - builder.field("notes"); + builder.field("notes", { extractor: (c: CipherViewLike) => CipherViewLikeUtils.getNotes(c) }); builder.field("login.username", { - extractor: (c: CipherView) => - c.type === CipherType.Login && c.login != null ? c.login.username : null, + extractor: (c: CipherViewLike) => { + const login = CipherViewLikeUtils.getLogin(c); + return login?.username ?? null; + }, + }); + builder.field("login.uris", { + boost: 2, + extractor: (c: CipherViewLike) => this.uriExtractor(c), + }); + builder.field("fields", { + extractor: (c: CipherViewLike) => this.fieldExtractor(c, false), + }); + builder.field("fields_joined", { + extractor: (c: CipherViewLike) => this.fieldExtractor(c, true), }); - builder.field("login.uris", { boost: 2, extractor: (c: CipherView) => this.uriExtractor(c) }); - builder.field("fields", { extractor: (c: CipherView) => this.fieldExtractor(c, false) }); - builder.field("fields_joined", { extractor: (c: CipherView) => this.fieldExtractor(c, true) }); builder.field("attachments", { - extractor: (c: CipherView) => this.attachmentExtractor(c, false), + extractor: (c: CipherViewLike) => this.attachmentExtractor(c, false), }); builder.field("attachments_joined", { - extractor: (c: CipherView) => this.attachmentExtractor(c, true), + extractor: (c: CipherViewLike) => this.attachmentExtractor(c, true), }); - builder.field("organizationid", { extractor: (c: CipherView) => c.organizationId }); + builder.field("organizationid", { extractor: (c: CipherViewLike) => c.organizationId }); ciphers = ciphers || []; ciphers.forEach((c) => builder.add(c)); const index = builder.build(); @@ -400,37 +412,44 @@ export class SearchService implements SearchServiceAbstraction { return await firstValueFrom(this.searchIsIndexing$(userId)); } - private fieldExtractor(c: CipherView, joined: boolean) { - if (!c.hasFields) { + private fieldExtractor(c: CipherViewLike, joined: boolean) { + const fields = CipherViewLikeUtils.getFields(c); + if (!fields || fields.length === 0) { return null; } - let fields: string[] = []; - c.fields.forEach((f) => { + let fieldStrings: string[] = []; + fields.forEach((f) => { if (f.name != null) { - fields.push(f.name); + fieldStrings.push(f.name); } - if (f.type === FieldType.Text && f.value != null) { - fields.push(f.value); + // For CipherListView, value is only populated for Text fields + // For CipherView, we check the type explicitly + if (f.value != null) { + const fieldType = (f as { type?: FieldType }).type; + if (fieldType === undefined || fieldType === FieldType.Text) { + fieldStrings.push(f.value); + } } }); - fields = fields.filter((f) => f.trim() !== ""); - if (fields.length === 0) { + fieldStrings = fieldStrings.filter((f) => f.trim() !== ""); + if (fieldStrings.length === 0) { return null; } - return joined ? fields.join(" ") : fields; + return joined ? fieldStrings.join(" ") : fieldStrings; } - private attachmentExtractor(c: CipherView, joined: boolean) { - if (!c.hasAttachments) { + private attachmentExtractor(c: CipherViewLike, joined: boolean) { + const attachmentNames = CipherViewLikeUtils.getAttachmentNames(c); + if (!attachmentNames || attachmentNames.length === 0) { return null; } let attachments: string[] = []; - c.attachments.forEach((a) => { - if (a != null && a.fileName != null) { - if (joined && a.fileName.indexOf(".") > -1) { - attachments.push(a.fileName.substr(0, a.fileName.lastIndexOf("."))); + attachmentNames.forEach((fileName) => { + if (fileName != null) { + if (joined && fileName.indexOf(".") > -1) { + attachments.push(fileName.substring(0, fileName.lastIndexOf("."))); } else { - attachments.push(a.fileName); + attachments.push(fileName); } } }); @@ -441,43 +460,39 @@ export class SearchService implements SearchServiceAbstraction { return joined ? attachments.join(" ") : attachments; } - private uriExtractor(c: CipherView) { - if (c.type !== CipherType.Login || c.login == null || !c.login.hasUris) { + private uriExtractor(c: CipherViewLike) { + if (CipherViewLikeUtils.getType(c) !== CipherType.Login) { + return null; + } + const login = CipherViewLikeUtils.getLogin(c); + if (!login?.uris?.length) { return null; } const uris: string[] = []; - c.login.uris.forEach((u) => { + login.uris.forEach((u) => { if (u.uri == null || u.uri === "") { return; } - // Match ports + // Extract port from URI const portMatch = u.uri.match(/:(\d+)(?:[/?#]|$)/); const port = portMatch?.[1]; - let uri = u.uri; - - if (u.hostname !== null) { - uris.push(u.hostname); + const hostname = CipherViewLikeUtils.getUriHostname(u); + if (hostname !== undefined) { + uris.push(hostname); if (port) { - uris.push(`${u.hostname}:${port}`); - uris.push(port); - } - return; - } else { - const slash = uri.indexOf("/"); - const hostPart = slash > -1 ? uri.substring(0, slash) : uri; - uris.push(hostPart); - if (port) { - uris.push(`${hostPart}`); + uris.push(`${hostname}:${port}`); uris.push(port); } } + // Add processed URI (strip protocol and query params for non-regex matches) + let uri = u.uri; if (u.match !== UriMatchStrategy.RegularExpression) { const protocolIndex = uri.indexOf("://"); if (protocolIndex > -1) { - uri = uri.substr(protocolIndex + 3); + uri = uri.substring(protocolIndex + 3); } const queryIndex = uri.search(/\?|&|#/); if (queryIndex > -1) { @@ -486,6 +501,7 @@ export class SearchService implements SearchServiceAbstraction { } uris.push(uri); }); + return uris.length > 0 ? uris : null; } diff --git a/libs/common/src/vault/utils/cipher-view-like-utils.spec.ts b/libs/common/src/vault/utils/cipher-view-like-utils.spec.ts index 56b94fcf3ce..2a7bfac2970 100644 --- a/libs/common/src/vault/utils/cipher-view-like-utils.spec.ts +++ b/libs/common/src/vault/utils/cipher-view-like-utils.spec.ts @@ -651,4 +651,198 @@ describe("CipherViewLikeUtils", () => { expect(CipherViewLikeUtils.decryptionFailure(cipherListView)).toBe(false); }); }); + + describe("getNotes", () => { + describe("CipherView", () => { + it("returns notes when present", () => { + const cipherView = createCipherView(); + cipherView.notes = "This is a test note"; + + expect(CipherViewLikeUtils.getNotes(cipherView)).toBe("This is a test note"); + }); + + it("returns undefined when notes are not present", () => { + const cipherView = createCipherView(); + cipherView.notes = undefined; + + expect(CipherViewLikeUtils.getNotes(cipherView)).toBeUndefined(); + }); + }); + + describe("CipherListView", () => { + it("returns notes when present", () => { + const cipherListView = { + type: "secureNote", + notes: "List view notes", + } as CipherListView; + + expect(CipherViewLikeUtils.getNotes(cipherListView)).toBe("List view notes"); + }); + + it("returns undefined when notes are not present", () => { + const cipherListView = { + type: "secureNote", + } as CipherListView; + + expect(CipherViewLikeUtils.getNotes(cipherListView)).toBeUndefined(); + }); + }); + }); + + describe("getFields", () => { + describe("CipherView", () => { + it("returns fields when present", () => { + const cipherView = createCipherView(); + cipherView.fields = [ + { name: "Field1", value: "Value1" } as any, + { name: "Field2", value: "Value2" } as any, + ]; + + const fields = CipherViewLikeUtils.getFields(cipherView); + + expect(fields).toHaveLength(2); + expect(fields?.[0].name).toBe("Field1"); + expect(fields?.[0].value).toBe("Value1"); + expect(fields?.[1].name).toBe("Field2"); + expect(fields?.[1].value).toBe("Value2"); + }); + + it("returns empty array when fields array is empty", () => { + const cipherView = createCipherView(); + cipherView.fields = []; + + expect(CipherViewLikeUtils.getFields(cipherView)).toEqual([]); + }); + }); + + describe("CipherListView", () => { + it("returns fields when present", () => { + const cipherListView = { + type: { login: {} }, + fields: [ + { name: "Username", value: "user@example.com" }, + { name: "API Key", value: "abc123" }, + ], + } as CipherListView; + + const fields = CipherViewLikeUtils.getFields(cipherListView); + + expect(fields).toHaveLength(2); + expect(fields?.[0].name).toBe("Username"); + expect(fields?.[0].value).toBe("user@example.com"); + expect(fields?.[1].name).toBe("API Key"); + expect(fields?.[1].value).toBe("abc123"); + }); + + it("returns empty array when fields array is empty", () => { + const cipherListView = { + type: "secureNote", + fields: [], + } as unknown as CipherListView; + + expect(CipherViewLikeUtils.getFields(cipherListView)).toEqual([]); + }); + + it("returns undefined when fields are not present", () => { + const cipherListView = { + type: "secureNote", + } as CipherListView; + + expect(CipherViewLikeUtils.getFields(cipherListView)).toBeUndefined(); + }); + }); + }); + + describe("getAttachmentNames", () => { + describe("CipherView", () => { + it("returns attachment filenames when present", () => { + const cipherView = createCipherView(); + const attachment1 = new AttachmentView(); + attachment1.id = "1"; + attachment1.fileName = "document.pdf"; + const attachment2 = new AttachmentView(); + attachment2.id = "2"; + attachment2.fileName = "image.png"; + const attachment3 = new AttachmentView(); + attachment3.id = "3"; + attachment3.fileName = "spreadsheet.xlsx"; + cipherView.attachments = [attachment1, attachment2, attachment3]; + + const attachmentNames = CipherViewLikeUtils.getAttachmentNames(cipherView); + + expect(attachmentNames).toEqual(["document.pdf", "image.png", "spreadsheet.xlsx"]); + }); + + it("filters out null and undefined filenames", () => { + const cipherView = createCipherView(); + const attachment1 = new AttachmentView(); + attachment1.id = "1"; + attachment1.fileName = "valid.pdf"; + const attachment2 = new AttachmentView(); + attachment2.id = "2"; + attachment2.fileName = null as any; + const attachment3 = new AttachmentView(); + attachment3.id = "3"; + attachment3.fileName = undefined; + const attachment4 = new AttachmentView(); + attachment4.id = "4"; + attachment4.fileName = "another.txt"; + cipherView.attachments = [attachment1, attachment2, attachment3, attachment4]; + + const attachmentNames = CipherViewLikeUtils.getAttachmentNames(cipherView); + + expect(attachmentNames).toEqual(["valid.pdf", "another.txt"]); + }); + + it("returns empty array when attachments have no filenames", () => { + const cipherView = createCipherView(); + const attachment1 = new AttachmentView(); + attachment1.id = "1"; + const attachment2 = new AttachmentView(); + attachment2.id = "2"; + cipherView.attachments = [attachment1, attachment2]; + + const attachmentNames = CipherViewLikeUtils.getAttachmentNames(cipherView); + + expect(attachmentNames).toEqual([]); + }); + + it("returns empty array for empty attachments array", () => { + const cipherView = createCipherView(); + cipherView.attachments = []; + + expect(CipherViewLikeUtils.getAttachmentNames(cipherView)).toEqual([]); + }); + }); + + describe("CipherListView", () => { + it("returns attachment names when present", () => { + const cipherListView = { + type: "secureNote", + attachmentNames: ["report.pdf", "photo.jpg", "data.csv"], + } as CipherListView; + + const attachmentNames = CipherViewLikeUtils.getAttachmentNames(cipherListView); + + expect(attachmentNames).toEqual(["report.pdf", "photo.jpg", "data.csv"]); + }); + + it("returns empty array when attachmentNames is empty", () => { + const cipherListView = { + type: "secureNote", + attachmentNames: [], + } as unknown as CipherListView; + + expect(CipherViewLikeUtils.getAttachmentNames(cipherListView)).toEqual([]); + }); + + it("returns undefined when attachmentNames is not present", () => { + const cipherListView = { + type: "secureNote", + } as CipherListView; + + expect(CipherViewLikeUtils.getAttachmentNames(cipherListView)).toBeUndefined(); + }); + }); + }); }); diff --git a/libs/common/src/vault/utils/cipher-view-like-utils.ts b/libs/common/src/vault/utils/cipher-view-like-utils.ts index 04adb8d4832..5359bfb958f 100644 --- a/libs/common/src/vault/utils/cipher-view-like-utils.ts +++ b/libs/common/src/vault/utils/cipher-view-like-utils.ts @@ -10,6 +10,7 @@ import { LoginUriView as LoginListUriView, } from "@bitwarden/sdk-internal"; +import { Utils } from "../../platform/misc/utils"; import { CipherType } from "../enums"; import { Cipher } from "../models/domain/cipher"; import { CardView } from "../models/view/card.view"; @@ -290,6 +291,71 @@ export class CipherViewLikeUtils { static decryptionFailure = (cipher: CipherViewLike): boolean => { return "decryptionFailure" in cipher ? cipher.decryptionFailure : false; }; + + /** + * Returns the notes from the cipher. + * + * @param cipher - The cipher to extract notes from (either `CipherView` or `CipherListView`) + * @returns The notes string if present, or `undefined` if not set + */ + static getNotes = (cipher: CipherViewLike): string | undefined => { + return cipher.notes; + }; + + /** + * Returns the fields from the cipher. + * + * @param cipher - The cipher to extract fields from (either `CipherView` or `CipherListView`) + * @returns Array of field objects with `name` and `value` properties, `undefined` if not set + */ + static getFields = ( + cipher: CipherViewLike, + ): { name?: string | null; value?: string | undefined }[] | undefined => { + if (this.isCipherListView(cipher)) { + return cipher.fields; + } + return cipher.fields; + }; + + /** + * Returns attachment filenames from the cipher. + * + * @param cipher - The cipher to extract attachment names from (either `CipherView` or `CipherListView`) + * @returns Array of attachment filenames, `undefined` if attachments are not present + */ + static getAttachmentNames = (cipher: CipherViewLike): string[] | undefined => { + if (this.isCipherListView(cipher)) { + return cipher.attachmentNames; + } + + return cipher.attachments + ?.map((a) => a.fileName) + .filter((name): name is string => name != null); + }; + + /** + * Extracts hostname from a login URI. + * + * @param uri - The URI object (either `LoginUriView` class or `LoginListUriView`) + * @returns The hostname if available, `undefined` otherwise + * + * @remarks + * - For `LoginUriView` (CipherView): Uses the built-in `hostname` getter + * - For `LoginListUriView` (CipherListView): Computes hostname using `Utils.getHostname()` + * - Returns `undefined` for RegularExpression match types or when hostname cannot be extracted + */ + static getUriHostname = (uri: LoginListUriView | LoginUriView): string | undefined => { + if ("hostname" in uri && typeof uri.hostname !== "undefined") { + return uri.hostname ?? undefined; + } + + if (uri.match !== UriMatchStrategy.RegularExpression && uri.uri) { + const hostname = Utils.getHostname(uri.uri); + return hostname === "" ? undefined : hostname; + } + + return undefined; + }; } /** diff --git a/libs/subscription/src/components/subscription-card/subscription-card.component.mdx b/libs/subscription/src/components/subscription-card/subscription-card.component.mdx index c9cc6df7263..d3bad6583f5 100644 --- a/libs/subscription/src/components/subscription-card/subscription-card.component.mdx +++ b/libs/subscription/src/components/subscription-card/subscription-card.component.mdx @@ -78,6 +78,7 @@ type SubscriptionCardAction = | "contact-support" | "manage-invoices" | "reinstate-subscription" + | "resubscribe" | "update-payment" | "upgrade-plan"; ``` @@ -279,7 +280,7 @@ Payment issue expired, subscription has been suspended: ``` -**Actions available:** Contact Support +**Actions available:** Resubscribe ### Past Due @@ -370,7 +371,7 @@ Subscription that has been canceled: ``` -**Note:** Canceled subscriptions display no callout or actions. +**Actions available:** Resubscribe ### Enterprise diff --git a/libs/subscription/src/components/subscription-card/subscription-card.component.spec.ts b/libs/subscription/src/components/subscription-card/subscription-card.component.spec.ts index cdb85360c74..f524c4b5c26 100644 --- a/libs/subscription/src/components/subscription-card/subscription-card.component.spec.ts +++ b/libs/subscription/src/components/subscription-card/subscription-card.component.spec.ts @@ -44,9 +44,11 @@ describe("SubscriptionCardComponent", () => { unpaid: "Unpaid", weCouldNotProcessYourPayment: "We could not process your payment", contactSupportShort: "Contact support", - yourSubscriptionHasExpired: "Your subscription has expired", + yourSubscriptionIsExpired: "Your subscription is expired", + yourSubscriptionIsCanceled: "Your subscription is canceled", yourSubscriptionIsScheduledToCancel: `Your subscription is scheduled to cancel on ${params[0]}`, reinstateSubscription: "Reinstate subscription", + resubscribe: "Resubscribe", upgradeYourPlan: "Upgrade your plan", premiumShareEvenMore: "Premium share even more", upgradeNow: "Upgrade now", @@ -253,7 +255,7 @@ describe("SubscriptionCardComponent", () => { expect(buttons[1].nativeElement.textContent.trim()).toBe("Contact support"); }); - it("should display incomplete_expired callout with contact support action", () => { + it("should display incomplete_expired callout with resubscribe action", () => { setupComponent({ ...baseSubscription, status: "incomplete_expired", @@ -265,18 +267,18 @@ describe("SubscriptionCardComponent", () => { expect(calloutData).toBeTruthy(); expect(calloutData!.type).toBe("danger"); expect(calloutData!.title).toBe("Expired"); - expect(calloutData!.description).toContain("Your subscription has expired"); + expect(calloutData!.description).toContain("Your subscription is expired"); expect(calloutData!.callsToAction?.length).toBe(1); const callout = fixture.debugElement.query(By.css("bit-callout")); expect(callout).toBeTruthy(); const description = callout.query(By.css("p")); - expect(description.nativeElement.textContent).toContain("Your subscription has expired"); + expect(description.nativeElement.textContent).toContain("Your subscription is expired"); const buttons = callout.queryAll(By.css("button")); expect(buttons.length).toBe(1); - expect(buttons[0].nativeElement.textContent.trim()).toBe("Contact support"); + expect(buttons[0].nativeElement.textContent.trim()).toBe("Resubscribe"); }); it("should display pending cancellation callout for active status with cancelAt", () => { @@ -364,15 +366,29 @@ describe("SubscriptionCardComponent", () => { expect(buttons[0].nativeElement.textContent.trim()).toBe("Manage invoices"); }); - it("should not display callout for canceled status", () => { + it("should display canceled callout with resubscribe action", () => { setupComponent({ ...baseSubscription, status: "canceled", canceled: new Date("2025-01-15"), }); + const calloutData = component.callout(); + expect(calloutData).toBeTruthy(); + expect(calloutData!.type).toBe("danger"); + expect(calloutData!.title).toBe("Canceled"); + expect(calloutData!.description).toContain("Your subscription is canceled"); + expect(calloutData!.callsToAction?.length).toBe(1); + const callout = fixture.debugElement.query(By.css("bit-callout")); - expect(callout).toBeFalsy(); + expect(callout).toBeTruthy(); + + const description = callout.query(By.css("p")); + expect(description.nativeElement.textContent).toContain("Your subscription is canceled"); + + const buttons = callout.queryAll(By.css("button")); + expect(buttons.length).toBe(1); + expect(buttons[0].nativeElement.textContent.trim()).toBe("Resubscribe"); }); it("should display unpaid callout with manage invoices action", () => { @@ -489,6 +505,39 @@ describe("SubscriptionCardComponent", () => { expect(emitSpy).toHaveBeenCalledWith("manage-invoices"); }); + + it("should emit resubscribe action when button is clicked for incomplete_expired status", () => { + setupComponent({ + ...baseSubscription, + status: "incomplete_expired", + suspension: new Date("2025-01-15"), + gracePeriod: 7, + }); + + const emitSpy = jest.spyOn(component.callToActionClicked, "emit"); + + const button = fixture.debugElement.query(By.css("bit-callout button")); + button.triggerEventHandler("click", { button: 0 }); + fixture.detectChanges(); + + expect(emitSpy).toHaveBeenCalledWith("resubscribe"); + }); + + it("should emit resubscribe action when button is clicked for canceled status", () => { + setupComponent({ + ...baseSubscription, + status: "canceled", + canceled: new Date("2025-01-15"), + }); + + const emitSpy = jest.spyOn(component.callToActionClicked, "emit"); + + const button = fixture.debugElement.query(By.css("bit-callout button")); + button.triggerEventHandler("click", { button: 0 }); + fixture.detectChanges(); + + expect(emitSpy).toHaveBeenCalledWith("resubscribe"); + }); }); describe("Cart summary header content", () => { diff --git a/libs/subscription/src/components/subscription-card/subscription-card.component.stories.ts b/libs/subscription/src/components/subscription-card/subscription-card.component.stories.ts index 32976c89cc2..3d99ded2e5c 100644 --- a/libs/subscription/src/components/subscription-card/subscription-card.component.stories.ts +++ b/libs/subscription/src/components/subscription-card/subscription-card.component.stories.ts @@ -51,10 +51,13 @@ export default { weCouldNotProcessYourPayment: "We could not process your payment. Please update your payment method or contact the support team for assistance.", contactSupportShort: "Contact Support", - yourSubscriptionHasExpired: - "Your subscription has expired. Please contact the support team for assistance.", + yourSubscriptionIsExpired: + "Your subscription is expired. Please resubscribe to continue using premium features.", + yourSubscriptionIsCanceled: + "Your subscription is canceled. Please resubscribe to continue using premium features.", yourSubscriptionIsScheduledToCancel: `Your subscription is scheduled to cancel on ${args[0]}. You can reinstate it anytime before then.`, reinstateSubscription: "Reinstate subscription", + resubscribe: "Resubscribe", upgradeYourPlan: "Upgrade your plan", premiumShareEvenMore: "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise.", diff --git a/libs/subscription/src/components/subscription-card/subscription-card.component.ts b/libs/subscription/src/components/subscription-card/subscription-card.component.ts index ebfb41df6c2..78d2c40eb3e 100644 --- a/libs/subscription/src/components/subscription-card/subscription-card.component.ts +++ b/libs/subscription/src/components/subscription-card/subscription-card.component.ts @@ -20,6 +20,7 @@ export const SubscriptionCardActions = { ContactSupport: "contact-support", ManageInvoices: "manage-invoices", ReinstateSubscription: "reinstate-subscription", + Resubscribe: "resubscribe", UpdatePayment: "update-payment", UpgradePlan: "upgrade-plan", } as const; @@ -154,12 +155,12 @@ export class SubscriptionCardComponent { return { title: this.i18nService.t("expired"), type: "danger", - description: this.i18nService.t("yourSubscriptionHasExpired"), + description: this.i18nService.t("yourSubscriptionIsExpired"), callsToAction: [ { - text: this.i18nService.t("contactSupportShort"), + text: this.i18nService.t("resubscribe"), buttonType: "unstyled", - action: SubscriptionCardActions.ContactSupport, + action: SubscriptionCardActions.Resubscribe, }, ], }; @@ -218,7 +219,18 @@ export class SubscriptionCardComponent { }; } case SubscriptionStatuses.Canceled: { - return null; + return { + title: this.i18nService.t("canceled"), + type: "danger", + description: this.i18nService.t("yourSubscriptionIsCanceled"), + callsToAction: [ + { + text: this.i18nService.t("resubscribe"), + buttonType: "unstyled", + action: SubscriptionCardActions.Resubscribe, + }, + ], + }; } case SubscriptionStatuses.Unpaid: { return { diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html index dc1894b0935..62fa65ae1e0 100644 --- a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html +++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html @@ -21,20 +21,6 @@ [originalSendView]="originalSendView" > - - {{ "sendLink" | i18n }} - - - - {{ "deletionDate" | i18n }} {{ "enterMultipleEmailsSeparatedByComma" | i18n }} } + + + {{ "sendLink" | i18n }} + + + diff --git a/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.html b/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.html index 17f1233d70e..62417dfb577 100644 --- a/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.html +++ b/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.html @@ -1,4 +1,4 @@ -
    +
    { { provide: BillingAccountProfileStateService, useValue: billingAccountProfileStateService }, { provide: AccountService, useValue: accountService }, ], - // FIXME(PM-18598): Replace unknownElements and unknownProperties with actual imports - errorOnUnknownProperties: false, }).compileComponents(); fixture = TestBed.createComponent(SendListFiltersComponent); diff --git a/libs/vault/src/services/archive-cipher-utilities.service.spec.ts b/libs/vault/src/services/archive-cipher-utilities.service.spec.ts index ea00f482987..3eff8997177 100644 --- a/libs/vault/src/services/archive-cipher-utilities.service.spec.ts +++ b/libs/vault/src/services/archive-cipher-utilities.service.spec.ts @@ -80,7 +80,7 @@ describe("ArchiveCipherUtilitiesService", () => { ); expect(toastService.showToast).toHaveBeenCalledWith({ variant: "success", - message: "itemWasSentToArchive", + message: "itemArchiveToast", }); }); @@ -106,7 +106,7 @@ describe("ArchiveCipherUtilitiesService", () => { ); expect(toastService.showToast).toHaveBeenCalledWith({ variant: "success", - message: "itemWasUnarchived", + message: "itemUnarchivedToast", }); }); diff --git a/libs/vault/src/services/archive-cipher-utilities.service.ts b/libs/vault/src/services/archive-cipher-utilities.service.ts index b747961a701..751eba96e73 100644 --- a/libs/vault/src/services/archive-cipher-utilities.service.ts +++ b/libs/vault/src/services/archive-cipher-utilities.service.ts @@ -58,7 +58,7 @@ export class ArchiveCipherUtilitiesService { ); this.toastService.showToast({ variant: "success", - message: this.i18nService.t("itemWasSentToArchive"), + message: this.i18nService.t("itemArchiveToast"), }); return cipherResponse; } catch { @@ -90,7 +90,7 @@ export class ArchiveCipherUtilitiesService { ); this.toastService.showToast({ variant: "success", - message: this.i18nService.t("itemWasUnarchived"), + message: this.i18nService.t("itemUnarchivedToast"), }); return cipherResponse; } catch { diff --git a/package-lock.json b/package-lock.json index f558d18e207..cac0b978a63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,8 @@ "@angular/platform-browser": "20.3.16", "@angular/platform-browser-dynamic": "20.3.16", "@angular/router": "20.3.16", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.522", - "@bitwarden/sdk-internal": "0.2.0-main.522", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.527", + "@bitwarden/sdk-internal": "0.2.0-main.527", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", @@ -4941,9 +4941,9 @@ "link": true }, "node_modules/@bitwarden/commercial-sdk-internal": { - "version": "0.2.0-main.522", - "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.522.tgz", - "integrity": "sha512-2wAbg30cGlDhSj14LaK2/ISuT91XPVeNgL/PU+eoxLhAehGKjAXdvZN3PSwFaAuaMbEFzlESvqC1pzzO4p/1zw==", + "version": "0.2.0-main.527", + "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.527.tgz", + "integrity": "sha512-4C4lwOgA2v184G2axUR5Jdb4UMXMhF52a/3c0lAZYbD/8Nid6jziE89nCa9hdfdazuPgWXhVFa3gPrhLZ4uTUQ==", "license": "BITWARDEN SOFTWARE DEVELOPMENT KIT LICENSE AGREEMENT", "dependencies": { "type-fest": "^4.41.0" @@ -5046,9 +5046,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.522", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.522.tgz", - "integrity": "sha512-E+YqqX/FvGF0vGx6sNJfYaMj88C+rVo51fQPMSHoOePdryFcKQSJX706Glv86OMLMXE7Ln5Lua8LJRftlF/EFQ==", + "version": "0.2.0-main.527", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.527.tgz", + "integrity": "sha512-dxPh4XjEGFDBASRBEd/JwUdoMAz10W/0QGygYkPwhKKGzJncfDEAgQ/KrT9wc36ycrDrOOspff7xs/vmmzI0+A==", "license": "GPL-3.0", "dependencies": { "type-fest": "^4.41.0" diff --git a/package.json b/package.json index bc1553c4622..7499a69f99c 100644 --- a/package.json +++ b/package.json @@ -161,8 +161,8 @@ "@angular/platform-browser": "20.3.16", "@angular/platform-browser-dynamic": "20.3.16", "@angular/router": "20.3.16", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.522", - "@bitwarden/sdk-internal": "0.2.0-main.522", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.527", + "@bitwarden/sdk-internal": "0.2.0-main.527", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", diff --git a/tsconfig.base.json b/tsconfig.base.json index 20940a2b699..ccfd1f720f4 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -25,6 +25,7 @@ "@bitwarden/auth/angular": ["./libs/auth/src/angular"], "@bitwarden/auth/common": ["./libs/auth/src/common"], "@bitwarden/auto-confirm": ["libs/auto-confirm/src/index.ts"], + "@bitwarden/auto-confirm/angular": ["libs/auto-confirm/src/angular"], "@bitwarden/billing": ["./libs/billing/src"], "@bitwarden/bit-common/*": ["./bitwarden_license/bit-common/src/*"], "@bitwarden/browser/*": ["./apps/browser/src/*"],