1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 09:13:33 +00:00

Merge remote-tracking branch 'origin/main' into pm-13115

This commit is contained in:
Jonathan Prusik
2025-01-06 16:36:32 -05:00
167 changed files with 6511 additions and 9369 deletions

View File

@@ -1193,7 +1193,7 @@ jobs:
if: | if: |
github.event_name != 'pull_request_target' github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-desktop') && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-desktop')
uses: slackapi/slack-github-action@fcfb566f8b0aab22203f066d80ca1d7e4b5d05b3 # v1.27.1 uses: slackapi/slack-github-action@485a9d42d3a73031f12ec201c457e2162c45d02d # v2.0.0
with: with:
channel-id: C074F5UESQ0 channel-id: C074F5UESQ0
payload: | payload: |

View File

@@ -2105,7 +2105,7 @@
"message": "Aikakatkaisutoiminnon vahvistus" "message": "Aikakatkaisutoiminnon vahvistus"
}, },
"autoFillAndSave": { "autoFillAndSave": {
"message": "Täytä automaattisesti ja tallenna" "message": "Automaattitäytä ja tallenna"
}, },
"fillAndSave": { "fillAndSave": {
"message": "Täytä ja tallenna" "message": "Täytä ja tallenna"
@@ -3482,7 +3482,7 @@
"description": "Aria label for the totp code displayed in the inline menu for autofill" "description": "Aria label for the totp code displayed in the inline menu for autofill"
}, },
"totpSecondsSpanAria": { "totpSecondsSpanAria": {
"message": "Time remaining before current TOTP expires", "message": "Aika jäljellä, ennen kuin nykyinen TOTP vanhenee",
"description": "Aria label for the totp seconds displayed in the inline menu for autofill" "description": "Aria label for the totp seconds displayed in the inline menu for autofill"
}, },
"fillCredentialsFor": { "fillCredentialsFor": {
@@ -4810,7 +4810,7 @@
"message": "Määritä kaksivaiheinen kirjautuminen" "message": "Määritä kaksivaiheinen kirjautuminen"
}, },
"newDeviceVerificationNoticeContentPage1": { "newDeviceVerificationNoticeContentPage1": {
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." "message": "Bitwarden lähettää tilisi sähköpostiosoitteeseen koodin, jolla voit vahvistaa kirjautumiset uusista laitteista helmikuusta 2025 alkaen."
}, },
"newDeviceVerificationNoticeContentPage2": { "newDeviceVerificationNoticeContentPage2": {
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."

View File

@@ -20,16 +20,16 @@
"message": "Crea account" "message": "Crea account"
}, },
"newToBitwarden": { "newToBitwarden": {
"message": "New to Bitwarden?" "message": "Nuovo in Bitwarden?"
}, },
"logInWithPasskey": { "logInWithPasskey": {
"message": "Log in with passkey" "message": "Accedi con passkey"
}, },
"useSingleSignOn": { "useSingleSignOn": {
"message": "Use single sign-on" "message": "Usa il Single Sign-On"
}, },
"welcomeBack": { "welcomeBack": {
"message": "Welcome back" "message": "Bentornato"
}, },
"setAStrongPassword": { "setAStrongPassword": {
"message": "Imposta una password robusta" "message": "Imposta una password robusta"
@@ -120,7 +120,7 @@
"message": "Copia password" "message": "Copia password"
}, },
"copyPassphrase": { "copyPassphrase": {
"message": "Copy passphrase" "message": "Copia passphrase"
}, },
"copyNote": { "copyNote": {
"message": "Copia nota" "message": "Copia nota"
@@ -153,13 +153,13 @@
"message": "Copia numero licenza" "message": "Copia numero licenza"
}, },
"copyPrivateKey": { "copyPrivateKey": {
"message": "Copy private key" "message": "Copia chiave privata"
}, },
"copyPublicKey": { "copyPublicKey": {
"message": "Copy public key" "message": "Copia chiave pubblica"
}, },
"copyFingerprint": { "copyFingerprint": {
"message": "Copy fingerprint" "message": "Copia impronta"
}, },
"copyCustomField": { "copyCustomField": {
"message": "Copia $FIELD$", "message": "Copia $FIELD$",
@@ -177,7 +177,7 @@
"message": "Copia note" "message": "Copia note"
}, },
"fill": { "fill": {
"message": "Fill", "message": "Riempi",
"description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible."
}, },
"autoFill": { "autoFill": {
@@ -193,10 +193,10 @@
"message": "Riempi automaticamente identità" "message": "Riempi automaticamente identità"
}, },
"fillVerificationCode": { "fillVerificationCode": {
"message": "Fill verification code" "message": "Riempi codice di verifica"
}, },
"fillVerificationCodeAria": { "fillVerificationCodeAria": {
"message": "Fill Verification Code", "message": "Riempi Codice di Verifica",
"description": "Aria label for the heading displayed the inline menu for totp code autofill" "description": "Aria label for the heading displayed the inline menu for totp code autofill"
}, },
"generatePasswordCopied": { "generatePasswordCopied": {
@@ -239,7 +239,7 @@
"message": "Aggiungi elemento" "message": "Aggiungi elemento"
}, },
"accountEmail": { "accountEmail": {
"message": "Account email" "message": "Email dell'account"
}, },
"requestHint": { "requestHint": {
"message": "Richiedi suggerimento" "message": "Richiedi suggerimento"
@@ -443,7 +443,7 @@
"message": "Genera password" "message": "Genera password"
}, },
"generatePassphrase": { "generatePassphrase": {
"message": "Generate passphrase" "message": "Genera passphrase"
}, },
"regeneratePassword": { "regeneratePassword": {
"message": "Rigenera password" "message": "Rigenera password"
@@ -530,7 +530,7 @@
"description": "Label for the avoid ambiguous characters checkbox." "description": "Label for the avoid ambiguous characters checkbox."
}, },
"generatorPolicyInEffect": { "generatorPolicyInEffect": {
"message": "Enterprise policy requirements have been applied to your generator options.", "message": "I requisiti di politica aziendale sono stati applicati alle opzioni del generatore.",
"description": "Indicates that a policy limits the credential generator screen." "description": "Indicates that a policy limits the credential generator screen."
}, },
"searchVault": { "searchVault": {
@@ -576,7 +576,7 @@
"message": "Note" "message": "Note"
}, },
"privateNote": { "privateNote": {
"message": "Private note" "message": "Nota privata"
}, },
"note": { "note": {
"message": "Nota" "message": "Nota"
@@ -600,7 +600,7 @@
"message": "Avvia il sito web" "message": "Avvia il sito web"
}, },
"launchWebsiteName": { "launchWebsiteName": {
"message": "Launch website $ITEMNAME$", "message": "Apri sito web $ITEMNAME$",
"placeholders": { "placeholders": {
"itemname": { "itemname": {
"content": "$1", "content": "$1",
@@ -633,7 +633,7 @@
"message": "Timeout della sessione" "message": "Timeout della sessione"
}, },
"vaultTimeoutHeader": { "vaultTimeoutHeader": {
"message": "Vault timeout" "message": "Timeout cassaforte"
}, },
"otherOptions": { "otherOptions": {
"message": "Altre opzioni" "message": "Altre opzioni"
@@ -651,13 +651,13 @@
"message": "La tua cassaforte è bloccata. Verifica la tua identità per continuare." "message": "La tua cassaforte è bloccata. Verifica la tua identità per continuare."
}, },
"yourVaultIsLockedV2": { "yourVaultIsLockedV2": {
"message": "Your vault is locked" "message": "Cassaforte bloccata"
}, },
"yourAccountIsLocked": { "yourAccountIsLocked": {
"message": "Your account is locked" "message": "Il tuo account è bloccato"
}, },
"or": { "or": {
"message": "or" "message": "o"
}, },
"unlock": { "unlock": {
"message": "Sblocca" "message": "Sblocca"
@@ -852,7 +852,7 @@
"message": "Accedi" "message": "Accedi"
}, },
"logInToBitwarden": { "logInToBitwarden": {
"message": "Log in to Bitwarden" "message": "Accedi a Bitwarden"
}, },
"restartRegistration": { "restartRegistration": {
"message": "Riprova la registrazione" "message": "Riprova la registrazione"
@@ -888,10 +888,10 @@
"message": "La verifica in due passaggi rende il tuo account più sicuro richiedendoti di verificare il tuo login usando un altro dispositivo come una chiave di sicurezza, app di autenticazione, SMS, telefonata, o email. Può essere abilitata nella cassaforte web su bitwarden.com. Vuoi visitare il sito?" "message": "La verifica in due passaggi rende il tuo account più sicuro richiedendoti di verificare il tuo login usando un altro dispositivo come una chiave di sicurezza, app di autenticazione, SMS, telefonata, o email. Può essere abilitata nella cassaforte web su bitwarden.com. Vuoi visitare il sito?"
}, },
"twoStepLoginConfirmationContent": { "twoStepLoginConfirmationContent": {
"message": "Make your account more secure by setting up two-step login in the Bitwarden web app." "message": "Rendi il tuo account più sicuro impostando l'autenticazione a due fattori nell'app web di Bitwarden."
}, },
"twoStepLoginConfirmationTitle": { "twoStepLoginConfirmationTitle": {
"message": "Continue to web app?" "message": "Aprire web app?"
}, },
"editedFolder": { "editedFolder": {
"message": "Cartella salvata" "message": "Cartella salvata"
@@ -1005,7 +1005,7 @@
"message": "Mostra le identità nella sezione Scheda per riempirle automaticamente." "message": "Mostra le identità nella sezione Scheda per riempirle automaticamente."
}, },
"clickToAutofillOnVault": { "clickToAutofillOnVault": {
"message": "Click items to autofill on Vault view" "message": "Clicca gli oggetti da riempire dalla sezione Cassaforte"
}, },
"clearClipboard": { "clearClipboard": {
"message": "Cancella appunti", "message": "Cancella appunti",
@@ -1126,7 +1126,7 @@
"description": "WARNING (should stay in capitalized letters if the language permits)" "description": "WARNING (should stay in capitalized letters if the language permits)"
}, },
"warningCapitalized": { "warningCapitalized": {
"message": "Warning", "message": "Attenzione",
"description": "Warning (should maintain locale-relevant capitalization)" "description": "Warning (should maintain locale-relevant capitalization)"
}, },
"confirmVaultExport": { "confirmVaultExport": {
@@ -1206,7 +1206,7 @@
"message": "File" "message": "File"
}, },
"fileToShare": { "fileToShare": {
"message": "File to share" "message": "File da condividere"
}, },
"selectFile": { "selectFile": {
"message": "Seleziona un file" "message": "Seleziona un file"
@@ -1317,10 +1317,10 @@
"message": "Inserisci il codice di verifica a 6 cifre dalla tua app di autenticazione." "message": "Inserisci il codice di verifica a 6 cifre dalla tua app di autenticazione."
}, },
"authenticationTimeout": { "authenticationTimeout": {
"message": "Authentication timeout" "message": "Timeout autenticazione"
}, },
"authenticationSessionTimedOut": { "authenticationSessionTimedOut": {
"message": "The authentication session timed out. Please restart the login process." "message": "La sessione di autenticazione è scaduta. Accedi di nuovo."
}, },
"enterVerificationCodeEmail": { "enterVerificationCodeEmail": {
"message": "Inserisci il codice di verifica a 6 cifre inviato a $EMAIL$.", "message": "Inserisci il codice di verifica a 6 cifre inviato a $EMAIL$.",
@@ -1440,7 +1440,7 @@
"message": "URL del server" "message": "URL del server"
}, },
"selfHostBaseUrl": { "selfHostBaseUrl": {
"message": "Self-host server URL", "message": "URL server autogestito",
"description": "Label for field requesting a self-hosted integration service URL" "description": "Label for field requesting a self-hosted integration service URL"
}, },
"apiUrl": { "apiUrl": {
@@ -1472,10 +1472,10 @@
"message": "Mostra suggerimenti di riempimento automatico nei campi del modulo" "message": "Mostra suggerimenti di riempimento automatico nei campi del modulo"
}, },
"showInlineMenuIdentitiesLabel": { "showInlineMenuIdentitiesLabel": {
"message": "Display identities as suggestions" "message": "Mostra identità come consigli"
}, },
"showInlineMenuCardsLabel": { "showInlineMenuCardsLabel": {
"message": "Display cards as suggestions" "message": "Mostra carte come consigli"
}, },
"showInlineMenuOnIconSelectionLabel": { "showInlineMenuOnIconSelectionLabel": {
"message": "Mostra suggerimenti quando l'icona è selezionata" "message": "Mostra suggerimenti quando l'icona è selezionata"
@@ -1768,7 +1768,7 @@
"message": "Identità" "message": "Identità"
}, },
"typeSshKey": { "typeSshKey": {
"message": "SSH key" "message": "Chiave SSH"
}, },
"newItemHeader": { "newItemHeader": {
"message": "Nuovo $TYPE$", "message": "Nuovo $TYPE$",
@@ -1801,13 +1801,13 @@
"message": "Cronologia delle password" "message": "Cronologia delle password"
}, },
"generatorHistory": { "generatorHistory": {
"message": "Generator history" "message": "Cronologia generatore"
}, },
"clearGeneratorHistoryTitle": { "clearGeneratorHistoryTitle": {
"message": "Clear generator history" "message": "Cancella cronologia generatore"
}, },
"cleargGeneratorHistoryDescription": { "cleargGeneratorHistoryDescription": {
"message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" "message": "Se continui, tutte le voci verranno eliminate definitivamente dalla cronologia del generatore. Vuoi continuare?"
}, },
"back": { "back": {
"message": "Indietro" "message": "Indietro"
@@ -1846,7 +1846,7 @@
"message": "Note sicure" "message": "Note sicure"
}, },
"sshKeys": { "sshKeys": {
"message": "SSH Keys" "message": "Chiavi SSH"
}, },
"clear": { "clear": {
"message": "Cancella", "message": "Cancella",
@@ -1929,10 +1929,10 @@
"message": "Cancella cronologia" "message": "Cancella cronologia"
}, },
"nothingToShow": { "nothingToShow": {
"message": "Nothing to show" "message": "Niente da mostrare"
}, },
"nothingGeneratedRecently": { "nothingGeneratedRecently": {
"message": "You haven't generated anything recently" "message": "Non hai generato niente di recente"
}, },
"remove": { "remove": {
"message": "Rimuovi" "message": "Rimuovi"
@@ -1993,16 +1993,16 @@
"message": "Sblocca con PIN" "message": "Sblocca con PIN"
}, },
"setYourPinTitle": { "setYourPinTitle": {
"message": "Set PIN" "message": "Imposta PIN"
}, },
"setYourPinButton": { "setYourPinButton": {
"message": "Set PIN" "message": "Imposta PIN"
}, },
"setYourPinCode": { "setYourPinCode": {
"message": "Imposta il tuo codice PIN per sbloccare Bitwarden. Le tue impostazioni PIN saranno resettate se esci completamente dall'app." "message": "Imposta il tuo codice PIN per sbloccare Bitwarden. Le tue impostazioni PIN saranno resettate se esci completamente dall'app."
}, },
"setYourPinCode1": { "setYourPinCode1": {
"message": "Your PIN will be used to unlock Bitwarden instead of your master password. Your PIN will reset if you ever fully log out of Bitwarden." "message": "Il tuo PIN sarà usato per sbloccare Bitwarden invece della password principale. Il PIN sarà ripristinato se ti disconnetterai completamente da Bitwarden."
}, },
"pinRequired": { "pinRequired": {
"message": "Codice PIN obbligatorio." "message": "Codice PIN obbligatorio."
@@ -2017,7 +2017,7 @@
"message": "Sblocca con i dati biometrici" "message": "Sblocca con i dati biometrici"
}, },
"unlockWithMasterPassword": { "unlockWithMasterPassword": {
"message": "Unlock with master password" "message": "Sblocca con password principale"
}, },
"awaitDesktop": { "awaitDesktop": {
"message": "In attesa di conferma dal desktop" "message": "In attesa di conferma dal desktop"
@@ -2029,7 +2029,7 @@
"message": "Blocca con la password principale al riavvio del browser" "message": "Blocca con la password principale al riavvio del browser"
}, },
"lockWithMasterPassOnRestart1": { "lockWithMasterPassOnRestart1": {
"message": "Require master password on browser restart" "message": "Richiedi password principale al riavvio del browser"
}, },
"selectOneCollection": { "selectOneCollection": {
"message": "Devi selezionare almeno una raccolta." "message": "Devi selezionare almeno una raccolta."
@@ -2067,7 +2067,7 @@
"message": "Azione timeout cassaforte" "message": "Azione timeout cassaforte"
}, },
"vaultTimeoutAction1": { "vaultTimeoutAction1": {
"message": "Timeout action" "message": "Azione al timeout"
}, },
"lock": { "lock": {
"message": "Blocca", "message": "Blocca",
@@ -2355,14 +2355,14 @@
"message": "Modifiche del dominio escluso salvate" "message": "Modifiche del dominio escluso salvate"
}, },
"limitSendViews": { "limitSendViews": {
"message": "Limit views" "message": "Limita visualizzazioni"
}, },
"limitSendViewsHint": { "limitSendViewsHint": {
"message": "No one can view this Send after the limit is reached.", "message": "Nessuno potrà vedere questo Send al raggiungimento del limite.",
"description": "Displayed under the limit views field on Send" "description": "Displayed under the limit views field on Send"
}, },
"limitSendViewsCount": { "limitSendViewsCount": {
"message": "$ACCESSCOUNT$ views left", "message": "$ACCESSCOUNT$ visualizzazioni rimaste",
"description": "Displayed under the limit views field on Send", "description": "Displayed under the limit views field on Send",
"placeholders": { "placeholders": {
"accessCount": { "accessCount": {
@@ -2376,14 +2376,14 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}, },
"sendDetails": { "sendDetails": {
"message": "Send details", "message": "Dettagli Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}, },
"sendTypeText": { "sendTypeText": {
"message": "Testo" "message": "Testo"
}, },
"sendTypeTextToShare": { "sendTypeTextToShare": {
"message": "Text to share" "message": "Testo da condividere"
}, },
"sendTypeFile": { "sendTypeFile": {
"message": "File" "message": "File"
@@ -2393,7 +2393,7 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}, },
"hideTextByDefault": { "hideTextByDefault": {
"message": "Hide text by default" "message": "Nascondi testo come default"
}, },
"expired": { "expired": {
"message": "Scaduto" "message": "Scaduto"
@@ -2440,7 +2440,7 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}, },
"deleteSendPermanentConfirmation": { "deleteSendPermanentConfirmation": {
"message": "Are you sure you want to permanently delete this Send?", "message": "Sicuro di voler eliminare definitivamente questo Send?",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}, },
"editSend": { "editSend": {
@@ -2451,7 +2451,7 @@
"message": "Data di eliminazione" "message": "Data di eliminazione"
}, },
"deletionDateDescV2": { "deletionDateDescV2": {
"message": "The Send will be permanently deleted on this date.", "message": "Il Send sarà cancellato definitivamente in questa data.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}, },
"expirationDate": { "expirationDate": {
@@ -2473,7 +2473,7 @@
"message": "Personalizzato" "message": "Personalizzato"
}, },
"sendPasswordDescV3": { "sendPasswordDescV3": {
"message": "Add an optional password for recipients to access this Send.", "message": "Richiedi ai destinatari una password opzionale per aprire questo Send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}, },
"createSend": { "createSend": {
@@ -2500,11 +2500,11 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}, },
"sendExpiresInHoursSingle": { "sendExpiresInHoursSingle": {
"message": "The Send will be available to anyone with the link for the next 1 hour.", "message": "Il Send sarà disponibile a chiunque con il link per la prossima ora.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}, },
"sendExpiresInHours": { "sendExpiresInHours": {
"message": "The Send will be available to anyone with the link for the next $HOURS$ hours.", "message": "Il Send sarà disponibile a chiunque con il link per le prossime $HOURS$ ore.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.",
"placeholders": { "placeholders": {
"hours": { "hours": {
@@ -2514,11 +2514,11 @@
} }
}, },
"sendExpiresInDaysSingle": { "sendExpiresInDaysSingle": {
"message": "The Send will be available to anyone with the link for the next 1 day.", "message": "Il Send sarà disponibile a chiunque con il link per il prossimo giorno.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}, },
"sendExpiresInDays": { "sendExpiresInDays": {
"message": "The Send will be available to anyone with the link for the next $DAYS$ days.", "message": "Il Send sarà disponibile a chiunque con il link per i prossimi $DAYS$ giorni.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.",
"placeholders": { "placeholders": {
"days": { "days": {
@@ -2536,11 +2536,11 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}, },
"sendFilePopoutDialogText": { "sendFilePopoutDialogText": {
"message": "Pop out extension?", "message": "Scollegare estensione?",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}, },
"sendFilePopoutDialogDesc": { "sendFilePopoutDialogDesc": {
"message": "To create a file Send, you need to pop out the extension to a new window.", "message": "Per creare un file Send, devi scollegare l'estensione in una nuova finestra.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}, },
"sendLinuxChromiumFileWarning": { "sendLinuxChromiumFileWarning": {
@@ -2553,7 +2553,7 @@
"message": "Per scegliere un file usando Safari, apri una nuova finestra cliccando questo banner." "message": "Per scegliere un file usando Safari, apri una nuova finestra cliccando questo banner."
}, },
"popOut": { "popOut": {
"message": "Pop out" "message": "Scollega"
}, },
"sendFileCalloutHeader": { "sendFileCalloutHeader": {
"message": "Prima di iniziare" "message": "Prima di iniziare"
@@ -2574,7 +2574,7 @@
"message": "Si è verificato un errore durante il salvataggio delle date di eliminazione e scadenza." "message": "Si è verificato un errore durante il salvataggio delle date di eliminazione e scadenza."
}, },
"hideYourEmail": { "hideYourEmail": {
"message": "Hide your email address from viewers." "message": "Nascondi il tuo indirizzo email ai visualizzatori."
}, },
"passwordPrompt": { "passwordPrompt": {
"message": "Richiedi di inserire la password principale di nuovo per visualizzare questo elemento" "message": "Richiedi di inserire la password principale di nuovo per visualizzare questo elemento"
@@ -2631,7 +2631,7 @@
"description": "Used as a card title description on the set password page to explain why the user is there" "description": "Used as a card title description on the set password page to explain why the user is there"
}, },
"cardMetrics": { "cardMetrics": {
"message": "out of $TOTAL$", "message": "di $TOTAL$",
"placeholders": { "placeholders": {
"total": { "total": {
"content": "$1", "content": "$1",
@@ -2650,7 +2650,7 @@
"message": "Minuti" "message": "Minuti"
}, },
"vaultTimeoutPolicyAffectingOptions": { "vaultTimeoutPolicyAffectingOptions": {
"message": "Enterprise policy requirements have been applied to your timeout options" "message": "I requisiti di politica aziendale sono stati applicati alle opzioni di timeout"
}, },
"vaultTimeoutPolicyInEffect": { "vaultTimeoutPolicyInEffect": {
"message": "Le politiche della tua organizzazione hanno impostato il timeout massimo consentito della tua cassaforte su $HOURS$ ore e $MINUTES$ minuti.", "message": "Le politiche della tua organizzazione hanno impostato il timeout massimo consentito della tua cassaforte su $HOURS$ ore e $MINUTES$ minuti.",
@@ -2666,7 +2666,7 @@
} }
}, },
"vaultTimeoutPolicyInEffect1": { "vaultTimeoutPolicyInEffect1": {
"message": "$HOURS$ hour(s) and $MINUTES$ minute(s) maximum.", "message": "Al massimo $HOURS$ ora/e e $MINUTES$ minuto/i.",
"placeholders": { "placeholders": {
"hours": { "hours": {
"content": "$1", "content": "$1",
@@ -2679,7 +2679,7 @@
} }
}, },
"vaultTimeoutPolicyMaximumError": { "vaultTimeoutPolicyMaximumError": {
"message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", "message": "Il timeout supera la restrizione impostata dalla tua organizzazione: massimo $HOURS$ ora/e e $MINUTES$ minuto/i",
"placeholders": { "placeholders": {
"hours": { "hours": {
"content": "$1", "content": "$1",
@@ -2793,10 +2793,10 @@
"message": "Genera nome utente" "message": "Genera nome utente"
}, },
"generateEmail": { "generateEmail": {
"message": "Generate email" "message": "Genera email"
}, },
"spinboxBoundariesHint": { "spinboxBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$.", "message": "Il valore deve essere compreso tra $MIN$ e $MAX$.",
"description": "Explains spin box minimum and maximum values to the user", "description": "Explains spin box minimum and maximum values to the user",
"placeholders": { "placeholders": {
"min": { "min": {
@@ -2810,7 +2810,7 @@
} }
}, },
"passwordLengthRecommendationHint": { "passwordLengthRecommendationHint": {
"message": " Use $RECOMMENDED$ characters or more to generate a strong password.", "message": " Usa $RECOMMENDED$ caratteri o più per generare una password forte.",
"description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": { "placeholders": {
"recommended": { "recommended": {
@@ -2820,7 +2820,7 @@
} }
}, },
"passphraseNumWordsRecommendationHint": { "passphraseNumWordsRecommendationHint": {
"message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", "message": " Usa $RECOMMENDED$ parole o più per generare una passphrase forte.",
"description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": { "placeholders": {
"recommended": { "recommended": {
@@ -2861,11 +2861,11 @@
"message": "Genera un alias email con un servizio di inoltro esterno." "message": "Genera un alias email con un servizio di inoltro esterno."
}, },
"forwarderDomainName": { "forwarderDomainName": {
"message": "Email domain", "message": "Dominio email",
"description": "Labels the domain name email forwarder service option" "description": "Labels the domain name email forwarder service option"
}, },
"forwarderDomainNameHint": { "forwarderDomainNameHint": {
"message": "Choose a domain that is supported by the selected service", "message": "Scegli un dominio supportato dal servizio selezionato",
"description": "Guidance provided for email forwarding services that support multiple email domains." "description": "Guidance provided for email forwarding services that support multiple email domains."
}, },
"forwarderError": { "forwarderError": {
@@ -3068,25 +3068,25 @@
"message": "Invia notifica di nuovo" "message": "Invia notifica di nuovo"
}, },
"viewAllLogInOptions": { "viewAllLogInOptions": {
"message": "View all log in options" "message": "Visualizza tutte le opzioni di accesso"
}, },
"viewAllLoginOptionsV1": { "viewAllLoginOptionsV1": {
"message": "View all log in options" "message": "Visualizza tutte le opzioni di accesso"
}, },
"notificationSentDevice": { "notificationSentDevice": {
"message": "Una notifica è stata inviata al tuo dispositivo." "message": "Una notifica è stata inviata al tuo dispositivo."
}, },
"aNotificationWasSentToYourDevice": { "aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device" "message": "Una notifica è stata inviata al tuo dispositivo"
}, },
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" "message": "Assicurati che il tuo account sia sbloccato e che la frase dell'impronta digitale corrisponda nell'altro dispositivo"
}, },
"youWillBeNotifiedOnceTheRequestIsApproved": { "youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved" "message": "Sarai notificato una volta che la richiesta sarà approvata"
}, },
"needAnotherOptionV1": { "needAnotherOptionV1": {
"message": "Need another option?" "message": "Bisogno di un'altra opzione?"
}, },
"loginInitiated": { "loginInitiated": {
"message": "Accesso avviato" "message": "Accesso avviato"
@@ -3182,16 +3182,16 @@
"message": "Si apre in una nuova finestra" "message": "Si apre in una nuova finestra"
}, },
"rememberThisDeviceToMakeFutureLoginsSeamless": { "rememberThisDeviceToMakeFutureLoginsSeamless": {
"message": "Remember this device to make future logins seamless" "message": "Ricorda questo dispositivo per rendere immediati i futuri accessi"
}, },
"deviceApprovalRequired": { "deviceApprovalRequired": {
"message": "Approvazione del dispositivo obbligatoria. Seleziona un'opzione di approvazione:" "message": "Approvazione del dispositivo obbligatoria. Seleziona un'opzione di approvazione:"
}, },
"deviceApprovalRequiredV2": { "deviceApprovalRequiredV2": {
"message": "Device approval required" "message": "Approvazione dispositivo richiesta"
}, },
"selectAnApprovalOptionBelow": { "selectAnApprovalOptionBelow": {
"message": "Select an approval option below" "message": "Seleziona un'opzione di approvazione sotto"
}, },
"rememberThisDevice": { "rememberThisDevice": {
"message": "Ricorda questo dispositivo" "message": "Ricorda questo dispositivo"
@@ -3267,7 +3267,7 @@
"message": "Email utente mancante" "message": "Email utente mancante"
}, },
"activeUserEmailNotFoundLoggingYouOut": { "activeUserEmailNotFoundLoggingYouOut": {
"message": "Active user email not found. Logging you out." "message": "Email utente attiva non trovata. Logout in corso."
}, },
"deviceTrusted": { "deviceTrusted": {
"message": "Dispositivo fidato" "message": "Dispositivo fidato"
@@ -3478,11 +3478,11 @@
"description": "Screen reader text (aria-label) for unlock account button in overlay" "description": "Screen reader text (aria-label) for unlock account button in overlay"
}, },
"totpCodeAria": { "totpCodeAria": {
"message": "Time-based One-Time Password Verification Code", "message": "Codice di Verifica One-Time a tempo",
"description": "Aria label for the totp code displayed in the inline menu for autofill" "description": "Aria label for the totp code displayed in the inline menu for autofill"
}, },
"totpSecondsSpanAria": { "totpSecondsSpanAria": {
"message": "Time remaining before current TOTP expires", "message": "Tempo rimasto prima che l'attuale TOTP scada",
"description": "Aria label for the totp seconds displayed in the inline menu for autofill" "description": "Aria label for the totp seconds displayed in the inline menu for autofill"
}, },
"fillCredentialsFor": { "fillCredentialsFor": {
@@ -3711,10 +3711,10 @@
"message": "Passkey" "message": "Passkey"
}, },
"accessing": { "accessing": {
"message": "Accessing" "message": "Accedendo a"
}, },
"loggedInExclamation": { "loggedInExclamation": {
"message": "Logged in!" "message": "Accesso effettuato!"
}, },
"passkeyNotCopied": { "passkeyNotCopied": {
"message": "La passkey non sarà copiata" "message": "La passkey non sarà copiata"
@@ -3741,7 +3741,7 @@
"message": "Nessun login corrispondente per questo sito" "message": "Nessun login corrispondente per questo sito"
}, },
"searchSavePasskeyNewLogin": { "searchSavePasskeyNewLogin": {
"message": "Search or save passkey as new login" "message": "Cerca o salva la passkey come nuovo login"
}, },
"confirm": { "confirm": {
"message": "Conferma" "message": "Conferma"
@@ -4208,13 +4208,13 @@
"message": "Filtri" "message": "Filtri"
}, },
"filterVault": { "filterVault": {
"message": "Filter vault" "message": "Filtra cassaforte"
}, },
"filterApplied": { "filterApplied": {
"message": "One filter applied" "message": "Un filtro applicato"
}, },
"filterAppliedPlural": { "filterAppliedPlural": {
"message": "$COUNT$ filters applied", "message": "$COUNT$ filtri applicati",
"placeholders": { "placeholders": {
"count": { "count": {
"content": "$1", "content": "$1",
@@ -4328,7 +4328,7 @@
"message": "Abilita animazioni" "message": "Abilita animazioni"
}, },
"showAnimations": { "showAnimations": {
"message": "Show animations" "message": "Mostra animazioni"
}, },
"addAccount": { "addAccount": {
"message": "Aggiungi account" "message": "Aggiungi account"
@@ -4546,13 +4546,13 @@
"message": "Posizione elemento" "message": "Posizione elemento"
}, },
"fileSend": { "fileSend": {
"message": "File Send" "message": "Send di File"
}, },
"fileSends": { "fileSends": {
"message": "Send File" "message": "Send File"
}, },
"textSend": { "textSend": {
"message": "Text Send" "message": "Send di Testo"
}, },
"textSends": { "textSends": {
"message": "Send Testo" "message": "Send Testo"
@@ -4570,7 +4570,7 @@
"message": "Mostra il numero di suggerimenti di riempimento automatico sull'icona dell'estensione" "message": "Mostra il numero di suggerimenti di riempimento automatico sull'icona dell'estensione"
}, },
"showQuickCopyActions": { "showQuickCopyActions": {
"message": "Show quick copy actions on Vault" "message": "Mostra azioni di copia rapida nella Cassaforte"
}, },
"systemDefault": { "systemDefault": {
"message": "Predefinito del sistema" "message": "Predefinito del sistema"
@@ -4579,37 +4579,37 @@
"message": "I requisiti della policy aziendale sono stati applicati a questa impostazione" "message": "I requisiti della policy aziendale sono stati applicati a questa impostazione"
}, },
"sshPrivateKey": { "sshPrivateKey": {
"message": "Private key" "message": "Chiave privata"
}, },
"sshPublicKey": { "sshPublicKey": {
"message": "Public key" "message": "Chiave pubblica"
}, },
"sshFingerprint": { "sshFingerprint": {
"message": "Fingerprint" "message": "Impronta digitale"
}, },
"sshKeyAlgorithm": { "sshKeyAlgorithm": {
"message": "Key type" "message": "Tipo di chiave"
}, },
"sshKeyAlgorithmED25519": { "sshKeyAlgorithmED25519": {
"message": "ED25519" "message": "ED25519"
}, },
"sshKeyAlgorithmRSA2048": { "sshKeyAlgorithmRSA2048": {
"message": "RSA 2048-Bit" "message": "RSA a 2048 bit"
}, },
"sshKeyAlgorithmRSA3072": { "sshKeyAlgorithmRSA3072": {
"message": "RSA 3072-Bit" "message": "RSA a 3072 bit"
}, },
"sshKeyAlgorithmRSA4096": { "sshKeyAlgorithmRSA4096": {
"message": "RSA 4096-Bit" "message": "RSA a 4096 bit"
}, },
"retry": { "retry": {
"message": "Retry" "message": "Riprova"
}, },
"vaultCustomTimeoutMinimum": { "vaultCustomTimeoutMinimum": {
"message": "Minimum custom timeout is 1 minute." "message": "Il timeout personalizzato minimo è 1 minuto."
}, },
"additionalContentAvailable": { "additionalContentAvailable": {
"message": "Additional content is available" "message": "Sono disponibili ulteriori contenuti"
}, },
"fileSavedToDevice": { "fileSavedToDevice": {
"message": "File salvato sul dispositivo. Gestisci dai download del dispositivo." "message": "File salvato sul dispositivo. Gestisci dai download del dispositivo."
@@ -4642,22 +4642,22 @@
"message": "Non hai i permessi per modificare questo elemento" "message": "Non hai i permessi per modificare questo elemento"
}, },
"authenticating": { "authenticating": {
"message": "Authenticating" "message": "Autenticazione"
}, },
"fillGeneratedPassword": { "fillGeneratedPassword": {
"message": "Fill generated password", "message": "Riempi password generata",
"description": "Heading for the password generator within the inline menu" "description": "Heading for the password generator within the inline menu"
}, },
"passwordRegenerated": { "passwordRegenerated": {
"message": "Password regenerated", "message": "Password rigenerata",
"description": "Notification message for when a password has been regenerated" "description": "Notification message for when a password has been regenerated"
}, },
"saveLoginToBitwarden": { "saveLoginToBitwarden": {
"message": "Save login to Bitwarden?", "message": "Salvare il login su Bitwarden?",
"description": "Confirmation message for saving a login to Bitwarden" "description": "Confirmation message for saving a login to Bitwarden"
}, },
"spaceCharacterDescriptor": { "spaceCharacterDescriptor": {
"message": "Space", "message": "Spazio",
"description": "Represents the space key in screen reader content as a readable word" "description": "Represents the space key in screen reader content as a readable word"
}, },
"tildeCharacterDescriptor": { "tildeCharacterDescriptor": {
@@ -4669,157 +4669,157 @@
"description": "Represents the ` key in screen reader content as a readable word" "description": "Represents the ` key in screen reader content as a readable word"
}, },
"exclamationCharacterDescriptor": { "exclamationCharacterDescriptor": {
"message": "Exclamation mark", "message": "Punto esclamativo",
"description": "Represents the ! key in screen reader content as a readable word" "description": "Represents the ! key in screen reader content as a readable word"
}, },
"atSignCharacterDescriptor": { "atSignCharacterDescriptor": {
"message": "At sign", "message": "Chiocciola",
"description": "Represents the @ key in screen reader content as a readable word" "description": "Represents the @ key in screen reader content as a readable word"
}, },
"hashSignCharacterDescriptor": { "hashSignCharacterDescriptor": {
"message": "Hash sign", "message": "Cancelletto",
"description": "Represents the # key in screen reader content as a readable word" "description": "Represents the # key in screen reader content as a readable word"
}, },
"dollarSignCharacterDescriptor": { "dollarSignCharacterDescriptor": {
"message": "Dollar sign", "message": "Simbolo del dollaro",
"description": "Represents the $ key in screen reader content as a readable word" "description": "Represents the $ key in screen reader content as a readable word"
}, },
"percentSignCharacterDescriptor": { "percentSignCharacterDescriptor": {
"message": "Percent sign", "message": "Segno di percentuale",
"description": "Represents the % key in screen reader content as a readable word" "description": "Represents the % key in screen reader content as a readable word"
}, },
"caretCharacterDescriptor": { "caretCharacterDescriptor": {
"message": "Caret", "message": "Accento circonflesso",
"description": "Represents the ^ key in screen reader content as a readable word" "description": "Represents the ^ key in screen reader content as a readable word"
}, },
"ampersandCharacterDescriptor": { "ampersandCharacterDescriptor": {
"message": "Ampersand", "message": "E commerciale",
"description": "Represents the & key in screen reader content as a readable word" "description": "Represents the & key in screen reader content as a readable word"
}, },
"asteriskCharacterDescriptor": { "asteriskCharacterDescriptor": {
"message": "Asterisk", "message": "Asterisco",
"description": "Represents the * key in screen reader content as a readable word" "description": "Represents the * key in screen reader content as a readable word"
}, },
"parenLeftCharacterDescriptor": { "parenLeftCharacterDescriptor": {
"message": "Left parenthesis", "message": "Parentesi sinistra",
"description": "Represents the ( key in screen reader content as a readable word" "description": "Represents the ( key in screen reader content as a readable word"
}, },
"parenRightCharacterDescriptor": { "parenRightCharacterDescriptor": {
"message": "Right parenthesis", "message": "Parentesi destra",
"description": "Represents the ) key in screen reader content as a readable word" "description": "Represents the ) key in screen reader content as a readable word"
}, },
"hyphenCharacterDescriptor": { "hyphenCharacterDescriptor": {
"message": "Underscore", "message": "Trattino basso",
"description": "Represents the _ key in screen reader content as a readable word" "description": "Represents the _ key in screen reader content as a readable word"
}, },
"underscoreCharacterDescriptor": { "underscoreCharacterDescriptor": {
"message": "Hyphen", "message": "Trattino",
"description": "Represents the - key in screen reader content as a readable word" "description": "Represents the - key in screen reader content as a readable word"
}, },
"plusCharacterDescriptor": { "plusCharacterDescriptor": {
"message": "Plus", "message": "P",
"description": "Represents the + key in screen reader content as a readable word" "description": "Represents the + key in screen reader content as a readable word"
}, },
"equalsCharacterDescriptor": { "equalsCharacterDescriptor": {
"message": "Equals", "message": "Uguale",
"description": "Represents the = key in screen reader content as a readable word" "description": "Represents the = key in screen reader content as a readable word"
}, },
"braceLeftCharacterDescriptor": { "braceLeftCharacterDescriptor": {
"message": "Left brace", "message": "Parentesi graffa aperta",
"description": "Represents the { key in screen reader content as a readable word" "description": "Represents the { key in screen reader content as a readable word"
}, },
"braceRightCharacterDescriptor": { "braceRightCharacterDescriptor": {
"message": "Right brace", "message": "Parentesi graffa chiusa",
"description": "Represents the } key in screen reader content as a readable word" "description": "Represents the } key in screen reader content as a readable word"
}, },
"bracketLeftCharacterDescriptor": { "bracketLeftCharacterDescriptor": {
"message": "Left bracket", "message": "Parentesi quadra aperta",
"description": "Represents the [ key in screen reader content as a readable word" "description": "Represents the [ key in screen reader content as a readable word"
}, },
"bracketRightCharacterDescriptor": { "bracketRightCharacterDescriptor": {
"message": "Right bracket", "message": "Parentesi quadra chiusa",
"description": "Represents the ] key in screen reader content as a readable word" "description": "Represents the ] key in screen reader content as a readable word"
}, },
"pipeCharacterDescriptor": { "pipeCharacterDescriptor": {
"message": "Pipe", "message": "Barra verticale",
"description": "Represents the | key in screen reader content as a readable word" "description": "Represents the | key in screen reader content as a readable word"
}, },
"backSlashCharacterDescriptor": { "backSlashCharacterDescriptor": {
"message": "Back slash", "message": "Barra rovesciata",
"description": "Represents the back slash key in screen reader content as a readable word" "description": "Represents the back slash key in screen reader content as a readable word"
}, },
"colonCharacterDescriptor": { "colonCharacterDescriptor": {
"message": "Colon", "message": "Due punti",
"description": "Represents the : key in screen reader content as a readable word" "description": "Represents the : key in screen reader content as a readable word"
}, },
"semicolonCharacterDescriptor": { "semicolonCharacterDescriptor": {
"message": "Semicolon", "message": "Punto e virgola",
"description": "Represents the ; key in screen reader content as a readable word" "description": "Represents the ; key in screen reader content as a readable word"
}, },
"doubleQuoteCharacterDescriptor": { "doubleQuoteCharacterDescriptor": {
"message": "Double quote", "message": "Doppi apici",
"description": "Represents the double quote key in screen reader content as a readable word" "description": "Represents the double quote key in screen reader content as a readable word"
}, },
"singleQuoteCharacterDescriptor": { "singleQuoteCharacterDescriptor": {
"message": "Single quote", "message": "Apostrofo",
"description": "Represents the ' key in screen reader content as a readable word" "description": "Represents the ' key in screen reader content as a readable word"
}, },
"lessThanCharacterDescriptor": { "lessThanCharacterDescriptor": {
"message": "Less than", "message": "Minore",
"description": "Represents the < key in screen reader content as a readable word" "description": "Represents the < key in screen reader content as a readable word"
}, },
"greaterThanCharacterDescriptor": { "greaterThanCharacterDescriptor": {
"message": "Greater than", "message": "Maggiore",
"description": "Represents the > key in screen reader content as a readable word" "description": "Represents the > key in screen reader content as a readable word"
}, },
"commaCharacterDescriptor": { "commaCharacterDescriptor": {
"message": "Comma", "message": "Virgola",
"description": "Represents the , key in screen reader content as a readable word" "description": "Represents the , key in screen reader content as a readable word"
}, },
"periodCharacterDescriptor": { "periodCharacterDescriptor": {
"message": "Period", "message": "Punto",
"description": "Represents the . key in screen reader content as a readable word" "description": "Represents the . key in screen reader content as a readable word"
}, },
"questionCharacterDescriptor": { "questionCharacterDescriptor": {
"message": "Question mark", "message": "Punto interrogativo",
"description": "Represents the ? key in screen reader content as a readable word" "description": "Represents the ? key in screen reader content as a readable word"
}, },
"forwardSlashCharacterDescriptor": { "forwardSlashCharacterDescriptor": {
"message": "Forward slash", "message": "Slash",
"description": "Represents the / key in screen reader content as a readable word" "description": "Represents the / key in screen reader content as a readable word"
}, },
"lowercaseAriaLabel": { "lowercaseAriaLabel": {
"message": "Lowercase" "message": "Minuscolo"
}, },
"uppercaseAriaLabel": { "uppercaseAriaLabel": {
"message": "Uppercase" "message": "Maiuscolo"
}, },
"generatedPassword": { "generatedPassword": {
"message": "Generated password" "message": "Password generata"
}, },
"compactMode": { "compactMode": {
"message": "Compact mode" "message": "Modalità compatta"
}, },
"beta": { "beta": {
"message": "Beta" "message": "Beta"
}, },
"importantNotice": { "importantNotice": {
"message": "Important notice" "message": "Avviso importante"
}, },
"setupTwoStepLogin": { "setupTwoStepLogin": {
"message": "Set up two-step login" "message": "Imposta accesso in due passaggi"
}, },
"newDeviceVerificationNoticeContentPage1": { "newDeviceVerificationNoticeContentPage1": {
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." "message": "Bitwarden invierà un codice all'email del tuo account per verificare gli accessi da nuovi dispositivi a partire da febbraio 2025."
}, },
"newDeviceVerificationNoticeContentPage2": { "newDeviceVerificationNoticeContentPage2": {
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." "message": "Puoi impostare l'accesso in due passaggi come modo alternativo per proteggere il tuo account, o cambiare la tua e-mail in una alla quale puoi accedere."
}, },
"remindMeLater": { "remindMeLater": {
"message": "Remind me later" "message": "Ricordamelo più tardi"
}, },
"newDeviceVerificationNoticePageOneFormContent": { "newDeviceVerificationNoticePageOneFormContent": {
"message": "Do you have reliable access to your email, $EMAIL$?", "message": "Riesci ancora ad accedere a questa email, $EMAIL$?",
"placeholders": { "placeholders": {
"email": { "email": {
"content": "$1", "content": "$1",
@@ -4828,24 +4828,24 @@
} }
}, },
"newDeviceVerificationNoticePageOneEmailAccessNo": { "newDeviceVerificationNoticePageOneEmailAccessNo": {
"message": "No, I do not" "message": "No, non riesco"
}, },
"newDeviceVerificationNoticePageOneEmailAccessYes": { "newDeviceVerificationNoticePageOneEmailAccessYes": {
"message": "Yes, I can reliably access my email" "message": "Sì, riesco ad accedere a questa email"
}, },
"turnOnTwoStepLogin": { "turnOnTwoStepLogin": {
"message": "Turn on two-step login" "message": "Attiva accesso in due passaggi"
}, },
"changeAcctEmail": { "changeAcctEmail": {
"message": "Change account email" "message": "Cambia l'email dell'account"
}, },
"extensionWidth": { "extensionWidth": {
"message": "Extension width" "message": "Larghezza estensione"
}, },
"wide": { "wide": {
"message": "Wide" "message": "Larga"
}, },
"extraWide": { "extraWide": {
"message": "Extra wide" "message": "Molto larga"
} }
} }

View File

@@ -1005,7 +1005,7 @@
"message": "自動入力を簡単にするために、タブページに ID アイテムを表示します" "message": "自動入力を簡単にするために、タブページに ID アイテムを表示します"
}, },
"clickToAutofillOnVault": { "clickToAutofillOnVault": {
"message": "Click items to autofill on Vault view" "message": "保管庫で、自動入力するアイテムをクリックしてください"
}, },
"clearClipboard": { "clearClipboard": {
"message": "クリップボードの消去", "message": "クリップボードの消去",
@@ -4810,10 +4810,10 @@
"message": "2段階認証を設定する" "message": "2段階認証を設定する"
}, },
"newDeviceVerificationNoticeContentPage1": { "newDeviceVerificationNoticeContentPage1": {
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." "message": "Bitwarden は2025年2月以降、新しいデバイスからのログイン時にアカウントのメールアドレスに確認コードを送信します。"
}, },
"newDeviceVerificationNoticeContentPage2": { "newDeviceVerificationNoticeContentPage2": {
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." "message": "代わりに2段階認証によるログインでアカウントを保護するか、メールアドレスをあなたがアクセスできるものに変更できます。"
}, },
"remindMeLater": { "remindMeLater": {
"message": "後で再通知" "message": "後で再通知"
@@ -4831,13 +4831,13 @@
"message": "いいえ、違います。" "message": "いいえ、違います。"
}, },
"newDeviceVerificationNoticePageOneEmailAccessYes": { "newDeviceVerificationNoticePageOneEmailAccessYes": {
"message": "Yes, I can reliably access my email" "message": "はい、メールアドレスには私が確実にアクセスできます"
}, },
"turnOnTwoStepLogin": { "turnOnTwoStepLogin": {
"message": "Turn on two-step login" "message": "2段階認証によるログインを有効にする"
}, },
"changeAcctEmail": { "changeAcctEmail": {
"message": "Change account email" "message": "アカウントのメールアドレスを変更する"
}, },
"extensionWidth": { "extensionWidth": {
"message": "拡張機能の幅" "message": "拡張機能の幅"

View File

@@ -4154,7 +4154,7 @@
"message": "Ek bilgiler" "message": "Ek bilgiler"
}, },
"itemHistory": { "itemHistory": {
"message": "Öğe geçmişi" "message": "Kayıt geçmişi"
}, },
"lastEdited": { "lastEdited": {
"message": "Son düzenlenme" "message": "Son düzenlenme"

View File

@@ -428,7 +428,7 @@
"description": "Short for 'credential generator'." "description": "Short for 'credential generator'."
}, },
"passGenInfo": { "passGenInfo": {
"message": "自动生成安全可靠唯一的登录密码。" "message": "自动为您的登录生成强大且唯一的密码。"
}, },
"bitWebVaultApp": { "bitWebVaultApp": {
"message": "Bitwarden 网页 App" "message": "Bitwarden 网页 App"
@@ -888,7 +888,7 @@
"message": "两步登录要求您从其他设备(例如安全钥匙、验证器 App、短信、电话或者电子邮件来验证您的登录这能使您的账户更加安全。两步登录需要在 bitwarden.com 网页版密码库中设置。现在访问此网站吗?" "message": "两步登录要求您从其他设备(例如安全钥匙、验证器 App、短信、电话或者电子邮件来验证您的登录这能使您的账户更加安全。两步登录需要在 bitwarden.com 网页版密码库中设置。现在访问此网站吗?"
}, },
"twoStepLoginConfirmationContent": { "twoStepLoginConfirmationContent": {
"message": "通过在 Bitwarden 网页 App 中设置两步登录,可以使您的账户更加安全。" "message": "在 Bitwarden 网页 App 中设置两步登录,您的账户更加安全。"
}, },
"twoStepLoginConfirmationTitle": { "twoStepLoginConfirmationTitle": {
"message": "前往网页 App 吗?" "message": "前往网页 App 吗?"
@@ -2123,7 +2123,7 @@
"message": "您仍然想要填充此登录信息吗?" "message": "您仍然想要填充此登录信息吗?"
}, },
"autofillIframeWarning": { "autofillIframeWarning": {
"message": "该表单由不同于您保存的登录 URI 域名托管。选择「确定」自动填充,或选择「取消」停止填充。" "message": "该表单由您保存的登录 URI 不同的域名托管。选择「确定」继续自动填充,或选择「取消」停止自动填充。"
}, },
"autofillIframeWarningTip": { "autofillIframeWarningTip": {
"message": "要防止以后出现此警告,请将此站点的 URI $HOSTNAME$ 保存到您的 Bitwarden 登录项目中。", "message": "要防止以后出现此警告,请将此站点的 URI $HOSTNAME$ 保存到您的 Bitwarden 登录项目中。",
@@ -3982,7 +3982,7 @@
"message": "自动填充建议" "message": "自动填充建议"
}, },
"autofillSuggestionsTip": { "autofillSuggestionsTip": {
"message": "保存此站点登录项目用来自动填充" "message": "此站点保存为登录项目以用于自动填充"
}, },
"yourVaultIsEmpty": { "yourVaultIsEmpty": {
"message": "您的密码库是空的" "message": "您的密码库是空的"

View File

@@ -1,140 +0,0 @@
<app-header>
<div class="left">
<button type="button" routerLink="/tabs/settings">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "accountSecurity" | i18n }}</span>
</h1>
<div class="right">
<app-pop-out></app-pop-out>
</div>
</app-header>
<main tabindex="-1" [formGroup]="form">
<div class="box list">
<h2 class="box-header">{{ "unlockMethods" | i18n }}</h2>
<div class="box-content single-line">
<div class="box-content-row box-content-row-checkbox" appBoxRow *ngIf="supportsBiometric">
<label for="biometric">{{ "unlockWithBiometrics" | i18n }}</label>
<input id="biometric" type="checkbox" formControlName="biometric" />
</div>
<div
class="box-content-row box-content-row-checkbox"
appBoxRow
*ngIf="supportsBiometric && this.form.value.biometric"
>
<label for="autoBiometricsPrompt">{{ "enableAutoBiometricsPrompt" | i18n }}</label>
<input
id="autoBiometricsPrompt"
type="checkbox"
(change)="updateAutoBiometricsPrompt()"
formControlName="enableAutoBiometricsPrompt"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="pin">{{ "unlockWithPin" | i18n }}</label>
<input id="pin" type="checkbox" formControlName="pin" />
</div>
</div>
</div>
<div class="box list">
<h2 class="box-header">{{ "sessionTimeoutHeader" | i18n }}</h2>
<div class="box-content single-line">
<app-callout type="info" *ngIf="vaultTimeoutPolicyCallout | async as policy">
<span *ngIf="policy.timeout && policy.action">
{{
"vaultTimeoutPolicyWithActionInEffect"
| i18n: policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n)
}}
</span>
<span *ngIf="policy.timeout && !policy.action">
{{ "vaultTimeoutPolicyInEffect" | i18n: policy.timeout.hours : policy.timeout.minutes }}
</span>
<span *ngIf="!policy.timeout && policy.action">
{{ "vaultTimeoutActionPolicyInEffect" | i18n: (policy.action | i18n) }}
</span>
</app-callout>
<app-vault-timeout-input
[vaultTimeoutOptions]="vaultTimeoutOptions"
[formControl]="form.controls.vaultTimeout"
ngDefaultControl
>
</app-vault-timeout-input>
<div class="box-content-row display-block" appBoxRow>
<label for="vaultTimeoutAction">{{ "vaultTimeoutAction" | i18n }}</label>
<select
id="vaultTimeoutAction"
name="VaultTimeoutActions"
formControlName="vaultTimeoutAction"
>
<option *ngFor="let action of availableVaultTimeoutActions" [ngValue]="action">
{{ action | i18n }}
</option>
</select>
</div>
<div
*ngIf="!availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)"
id="unlockMethodHelp"
class="box-footer"
>
{{ "unlockMethodNeededToChangeTimeoutActionDesc" | i18n }}
</div>
</div>
</div>
<div class="box list">
<h2 class="box-header">{{ "otherOptions" | i18n }}</h2>
<div class="box-content single-line">
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="fingerprint()"
>
<div class="row-main">{{ "fingerprintPhrase" | i18n }}</div>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="twoStep()"
>
<div class="row-main">{{ "twoStepLogin" | i18n }}</div>
<i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="changePassword()"
*ngIf="showChangeMasterPass"
>
<div class="row-main">{{ "changeMasterPassword" | i18n }}</div>
<i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
*ngIf="
!accountSwitcherEnabled && availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)
"
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="lock()"
>
<div class="row-main">{{ "lockNow" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
*ngIf="!accountSwitcherEnabled"
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="logOut()"
>
<div class="row-main">{{ "logOut" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
</div>
</div>
</main>

View File

@@ -1,499 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import {
BehaviorSubject,
combineLatest,
concatMap,
distinctUntilChanged,
filter,
firstValueFrom,
map,
Observable,
pairwise,
startWith,
Subject,
switchMap,
takeUntil,
} from "rxjs";
import { FingerprintDialogComponent } from "@bitwarden/auth/angular";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import {
VaultTimeout,
VaultTimeoutOption,
VaultTimeoutStringType,
} from "@bitwarden/common/types/vault-timeout.type";
import { DialogService } from "@bitwarden/components";
import { KeyService, BiometricStateService, BiometricsService } from "@bitwarden/key-management";
import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors";
import { BrowserApi } from "../../../platform/browser/browser-api";
import { enableAccountSwitching } from "../../../platform/flags";
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
import { SetPinComponent } from "../components/set-pin.component";
import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
@Component({
selector: "auth-account-security",
templateUrl: "account-security-v1.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class AccountSecurityComponent implements OnInit, OnDestroy {
protected readonly VaultTimeoutAction = VaultTimeoutAction;
availableVaultTimeoutActions: VaultTimeoutAction[] = [];
vaultTimeoutOptions: VaultTimeoutOption[];
vaultTimeoutPolicyCallout: Observable<{
timeout: { hours: string; minutes: string };
action: VaultTimeoutAction;
}>;
supportsBiometric: boolean;
showChangeMasterPass = true;
accountSwitcherEnabled = false;
form = this.formBuilder.group({
vaultTimeout: [null as VaultTimeout | null],
vaultTimeoutAction: [VaultTimeoutAction.Lock],
pin: [null as boolean | null],
biometric: false,
enableAutoBiometricsPrompt: true,
});
private refreshTimeoutSettings$ = new BehaviorSubject<void>(undefined);
private destroy$ = new Subject<void>();
constructor(
private accountService: AccountService,
private pinService: PinServiceAbstraction,
private policyService: PolicyService,
private formBuilder: FormBuilder,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private vaultTimeoutService: VaultTimeoutService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
public messagingService: MessagingService,
private environmentService: EnvironmentService,
private keyService: KeyService,
private stateService: StateService,
private userVerificationService: UserVerificationService,
private dialogService: DialogService,
private changeDetectorRef: ChangeDetectorRef,
private biometricStateService: BiometricStateService,
private biometricsService: BiometricsService,
) {
this.accountSwitcherEnabled = enableAccountSwitching();
}
async ngOnInit() {
const maximumVaultTimeoutPolicy = this.policyService.get$(PolicyType.MaximumVaultTimeout);
this.vaultTimeoutPolicyCallout = maximumVaultTimeoutPolicy.pipe(
filter((policy) => policy != null),
map((policy) => {
let timeout;
if (policy.data?.minutes) {
timeout = {
hours: Math.floor(policy.data?.minutes / 60).toString(),
minutes: (policy.data?.minutes % 60).toString(),
};
}
return { timeout: timeout, action: policy.data?.action };
}),
);
const showOnLocked =
!this.platformUtilsService.isFirefox() && !this.platformUtilsService.isSafari();
this.vaultTimeoutOptions = [
{ name: this.i18nService.t("immediately"), value: 0 },
{ name: this.i18nService.t("oneMinute"), value: 1 },
{ name: this.i18nService.t("fiveMinutes"), value: 5 },
{ name: this.i18nService.t("fifteenMinutes"), value: 15 },
{ name: this.i18nService.t("thirtyMinutes"), value: 30 },
{ name: this.i18nService.t("oneHour"), value: 60 },
{ name: this.i18nService.t("fourHours"), value: 240 },
];
if (showOnLocked) {
this.vaultTimeoutOptions.push({
name: this.i18nService.t("onLocked"),
value: VaultTimeoutStringType.OnLocked,
});
}
this.vaultTimeoutOptions.push({
name: this.i18nService.t("onRestart"),
value: VaultTimeoutStringType.OnRestart,
});
this.vaultTimeoutOptions.push({
name: this.i18nService.t("never"),
value: VaultTimeoutStringType.Never,
});
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
let timeout = await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(activeAccount.id),
);
if (timeout === VaultTimeoutStringType.OnLocked && !showOnLocked) {
timeout = VaultTimeoutStringType.OnRestart;
}
const initialValues = {
vaultTimeout: timeout,
vaultTimeoutAction: await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id),
),
pin: await this.pinService.isPinSet(activeAccount.id),
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
enableAutoBiometricsPrompt: await firstValueFrom(
this.biometricStateService.promptAutomatically$,
),
};
this.form.patchValue(initialValues, { emitEvent: false });
this.supportsBiometric = await this.biometricsService.supportsBiometric();
this.showChangeMasterPass = await this.userVerificationService.hasMasterPassword();
this.form.controls.vaultTimeout.valueChanges
.pipe(
startWith(initialValues.vaultTimeout), // emit to init pairwise
pairwise(),
concatMap(async ([previousValue, newValue]) => {
await this.saveVaultTimeout(previousValue, newValue);
}),
takeUntil(this.destroy$),
)
.subscribe();
this.form.controls.vaultTimeoutAction.valueChanges
.pipe(
startWith(initialValues.vaultTimeoutAction), // emit to init pairwise
pairwise(),
concatMap(async ([previousValue, newValue]) => {
await this.saveVaultTimeoutAction(previousValue, newValue);
}),
takeUntil(this.destroy$),
)
.subscribe();
this.form.controls.pin.valueChanges
.pipe(
concatMap(async (value) => {
await this.updatePin(value);
this.refreshTimeoutSettings$.next();
}),
takeUntil(this.destroy$),
)
.subscribe();
this.form.controls.biometric.valueChanges
.pipe(
distinctUntilChanged(),
concatMap(async (enabled) => {
await this.updateBiometric(enabled);
if (enabled) {
this.form.controls.enableAutoBiometricsPrompt.enable();
} else {
this.form.controls.enableAutoBiometricsPrompt.disable();
}
this.refreshTimeoutSettings$.next();
}),
takeUntil(this.destroy$),
)
.subscribe();
this.refreshTimeoutSettings$
.pipe(
switchMap(() =>
combineLatest([
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id),
]),
),
takeUntil(this.destroy$),
)
.subscribe(([availableActions, action]) => {
this.availableVaultTimeoutActions = availableActions;
this.form.controls.vaultTimeoutAction.setValue(action, { emitEvent: false });
// NOTE: The UI doesn't properly update without detect changes.
// I've even tried using an async pipe, but it still doesn't work. I'm not sure why.
// Using an async pipe means that we can't call `detectChanges` AFTER the data has change
// meaning that we are forced to use regular class variables instead of observables.
this.changeDetectorRef.detectChanges();
});
this.refreshTimeoutSettings$
.pipe(
switchMap(() =>
combineLatest([
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
maximumVaultTimeoutPolicy,
]),
),
takeUntil(this.destroy$),
)
.subscribe(([availableActions, policy]) => {
if (policy?.data?.action || availableActions.length <= 1) {
this.form.controls.vaultTimeoutAction.disable({ emitEvent: false });
} else {
this.form.controls.vaultTimeoutAction.enable({ emitEvent: false });
}
});
}
async saveVaultTimeout(previousValue: VaultTimeout, newValue: VaultTimeout) {
if (newValue === VaultTimeoutStringType.Never) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "warning" },
content: { key: "neverLockWarning" },
type: "warning",
});
if (!confirmed) {
this.form.controls.vaultTimeout.setValue(previousValue, { emitEvent: false });
return;
}
}
// The minTimeoutError does not apply to browser because it supports Immediately
// So only check for the policyError
if (this.form.controls.vaultTimeout.hasError("policyError")) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("vaultTimeoutTooLarge"),
);
return;
}
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
const vaultTimeoutAction = await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id),
);
await this.vaultTimeoutSettingsService.setVaultTimeoutOptions(
activeAccount.id,
newValue,
vaultTimeoutAction,
);
if (newValue === VaultTimeoutStringType.Never) {
this.messagingService.send("bgReseedStorage");
}
}
async saveVaultTimeoutAction(previousValue: VaultTimeoutAction, newValue: VaultTimeoutAction) {
if (newValue === VaultTimeoutAction.LogOut) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "vaultTimeoutLogOutConfirmationTitle" },
content: { key: "vaultTimeoutLogOutConfirmation" },
type: "warning",
});
if (!confirmed) {
this.form.controls.vaultTimeoutAction.setValue(previousValue, {
emitEvent: false,
});
return;
}
}
if (this.form.controls.vaultTimeout.hasError("policyError")) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("vaultTimeoutTooLarge"),
);
return;
}
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
await this.vaultTimeoutSettingsService.setVaultTimeoutOptions(
activeAccount.id,
this.form.value.vaultTimeout,
newValue,
);
this.refreshTimeoutSettings$.next();
}
async updatePin(value: boolean) {
if (value) {
const dialogRef = SetPinComponent.open(this.dialogService);
if (dialogRef == null) {
this.form.controls.pin.setValue(false, { emitEvent: false });
return;
}
const userHasPinSet = await firstValueFrom(dialogRef.closed);
this.form.controls.pin.setValue(userHasPinSet, { emitEvent: false });
} else {
await this.vaultTimeoutSettingsService.clear();
}
}
async updateBiometric(enabled: boolean) {
if (enabled && this.supportsBiometric) {
let granted;
try {
granted = await BrowserApi.requestPermission({ permissions: ["nativeMessaging"] });
} catch (e) {
// eslint-disable-next-line
console.error(e);
if (this.platformUtilsService.isFirefox() && BrowserPopupUtils.inSidebar(window)) {
await this.dialogService.openSimpleDialog({
title: { key: "nativeMessaginPermissionSidebarTitle" },
content: { key: "nativeMessaginPermissionSidebarDesc" },
acceptButtonText: { key: "ok" },
cancelButtonText: null,
type: "info",
});
this.form.controls.biometric.setValue(false);
return;
}
}
if (!granted) {
await this.dialogService.openSimpleDialog({
title: { key: "nativeMessaginPermissionErrorTitle" },
content: { key: "nativeMessaginPermissionErrorDesc" },
acceptButtonText: { key: "ok" },
cancelButtonText: null,
type: "danger",
});
this.form.controls.biometric.setValue(false);
return;
}
const awaitDesktopDialogRef = AwaitDesktopDialogComponent.open(this.dialogService);
const awaitDesktopDialogClosed = firstValueFrom(awaitDesktopDialogRef.closed);
await this.keyService.refreshAdditionalKeys();
await Promise.race([
awaitDesktopDialogClosed.then(async (result) => {
if (result !== true) {
this.form.controls.biometric.setValue(false);
}
}),
this.biometricsService
.authenticateBiometric()
.then((result) => {
this.form.controls.biometric.setValue(result);
if (!result) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorEnableBiometricTitle"),
this.i18nService.t("errorEnableBiometricDesc"),
);
}
})
.catch((e) => {
// Handle connection errors
this.form.controls.biometric.setValue(false);
const error = BiometricErrors[e.message as BiometricErrorTypes];
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.dialogService.openSimpleDialog({
title: { key: error.title },
content: { key: error.description },
acceptButtonText: { key: "ok" },
cancelButtonText: null,
type: "danger",
});
})
.finally(() => {
awaitDesktopDialogRef.close(true);
}),
]);
} else {
await this.biometricStateService.setBiometricUnlockEnabled(false);
await this.biometricStateService.setFingerprintValidated(false);
}
}
async updateAutoBiometricsPrompt() {
await this.biometricStateService.setPromptAutomatically(
this.form.value.enableAutoBiometricsPrompt,
);
}
async changePassword() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "continueToWebApp" },
content: { key: "changeMasterPasswordOnWebConfirmation" },
type: "info",
acceptButtonText: { key: "continue" },
});
if (confirmed) {
const env = await firstValueFrom(this.environmentService.environment$);
await BrowserApi.createNewTab(env.getWebVaultUrl());
}
}
async twoStep() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "twoStepLogin" },
content: { key: "twoStepLoginConfirmation" },
type: "info",
});
if (confirmed) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserApi.createNewTab("https://bitwarden.com/help/setup-two-step-login/");
}
}
async fingerprint() {
const fingerprint = await this.keyService.getFingerprint(await this.stateService.getUserId());
const dialogRef = FingerprintDialogComponent.open(this.dialogService, {
fingerprint,
});
return firstValueFrom(dialogRef.closed);
}
async lock() {
await this.vaultTimeoutService.lock();
}
async logOut() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "logOut" },
content: { key: "logOutConfirmation" },
type: "info",
});
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
if (confirmed) {
this.messagingService.send("logout", { userId: userId });
}
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@@ -60,7 +60,6 @@ import { BrowserApi } from "../../../platform/browser/browser-api";
import { enableAccountSwitching } from "../../../platform/flags"; import { enableAccountSwitching } from "../../../platform/flags";
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
import { SetPinComponent } from "../components/set-pin.component"; import { SetPinComponent } from "../components/set-pin.component";
@@ -82,7 +81,6 @@ import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
JslibModule, JslibModule,
LinkModule, LinkModule,
PopOutComponent, PopOutComponent,
PopupFooterComponent,
PopupHeaderComponent, PopupHeaderComponent,
PopupPageComponent, PopupPageComponent,
RouterModule, RouterModule,

View File

@@ -1,9 +1,4 @@
<ng-container *ngIf="show && !useRefreshVariant"> <ng-container *ngIf="show">
<button type="button" (click)="expand()" appA11yTitle="{{ 'popOutNewWindow' | i18n }}">
<i class="bwi bwi-external-link bwi-rotate-270 bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
</ng-container>
<ng-container *ngIf="show && useRefreshVariant">
<button <button
bitIconButton="bwi-popout" bitIconButton="bwi-popout"
size="small" size="small"

View File

@@ -2,8 +2,6 @@ import { CommonModule } from "@angular/common";
import { Component, Input, OnInit } from "@angular/core"; import { Component, Input, OnInit } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { IconButtonModule } from "@bitwarden/components"; import { IconButtonModule } from "@bitwarden/components";
@@ -17,16 +15,10 @@ import BrowserPopupUtils from "../browser-popup-utils";
}) })
export class PopOutComponent implements OnInit { export class PopOutComponent implements OnInit {
@Input() show = true; @Input() show = true;
useRefreshVariant = false;
constructor( constructor(private platformUtilsService: PlatformUtilsService) {}
private platformUtilsService: PlatformUtilsService,
private configService: ConfigService,
) {}
async ngOnInit() { async ngOnInit() {
this.useRefreshVariant = await this.configService.getFeatureFlag(FeatureFlag.ExtensionRefresh);
if (this.show) { if (this.show) {
if ( if (
(BrowserPopupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()) || (BrowserPopupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()) ||

View File

@@ -65,7 +65,6 @@ import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-req
import { RegisterComponent } from "../auth/popup/register.component"; import { RegisterComponent } from "../auth/popup/register.component";
import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
import { SetPasswordComponent } from "../auth/popup/set-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component";
import { AccountSecurityComponent as AccountSecurityV1Component } from "../auth/popup/settings/account-security-v1.component";
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
import { SsoComponentV1 } from "../auth/popup/sso-v1.component"; import { SsoComponentV1 } from "../auth/popup/sso-v1.component";
import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component"; import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component";
@@ -95,28 +94,16 @@ import { ExportBrowserV2Component } from "../tools/popup/settings/export/export-
import { ImportBrowserV2Component } from "../tools/popup/settings/import/import-browser-v2.component"; import { ImportBrowserV2Component } from "../tools/popup/settings/import/import-browser-v2.component";
import { SettingsV2Component } from "../tools/popup/settings/settings-v2.component"; import { SettingsV2Component } from "../tools/popup/settings/settings-v2.component";
import { clearVaultStateGuard } from "../vault/guards/clear-vault-state.guard"; import { clearVaultStateGuard } from "../vault/guards/clear-vault-state.guard";
import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component";
import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component";
import { CollectionsComponent } from "../vault/popup/components/vault/collections.component";
import { PasswordHistoryComponent } from "../vault/popup/components/vault/password-history.component";
import { ShareComponent } from "../vault/popup/components/vault/share.component";
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component";
import { ViewComponent } from "../vault/popup/components/vault/view.component";
import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/add-edit-v2.component"; import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/add-edit-v2.component";
import { AssignCollections } from "../vault/popup/components/vault-v2/assign-collections/assign-collections.component"; import { AssignCollections } from "../vault/popup/components/vault-v2/assign-collections/assign-collections.component";
import { AttachmentsV2Component } from "../vault/popup/components/vault-v2/attachments/attachments-v2.component"; import { AttachmentsV2Component } from "../vault/popup/components/vault-v2/attachments/attachments-v2.component";
import { PasswordHistoryV2Component } from "../vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component"; import { PasswordHistoryV2Component } from "../vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component";
import { VaultV2Component } from "../vault/popup/components/vault-v2/vault-v2.component";
import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component"; import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component";
import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component"; import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component";
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component"; import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component";
import { FoldersComponent } from "../vault/popup/settings/folders.component";
import { SyncComponent } from "../vault/popup/settings/sync.component";
import { TrashComponent } from "../vault/popup/settings/trash.component"; import { TrashComponent } from "../vault/popup/settings/trash.component";
import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component"; import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component";
import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component";
import { RouteElevation } from "./app-routing.animations"; import { RouteElevation } from "./app-routing.animations";
import { debounceNavigationGuard } from "./services/debounce-navigation.service"; import { debounceNavigationGuard } from "./services/debounce-navigation.service";
@@ -273,56 +260,43 @@ const routes: Routes = [
data: { elevation: 1 } satisfies RouteDataProperties, data: { elevation: 1 } satisfies RouteDataProperties,
}, },
{ {
path: "ciphers",
component: VaultItemsComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
},
...extensionRefreshSwap(ViewComponent, ViewV2Component, {
path: "view-cipher", path: "view-cipher",
component: ViewV2Component,
canActivate: [authGuard], canActivate: [authGuard],
data: { data: {
// Above "trash" // Above "trash"
elevation: 3, elevation: 3,
} satisfies RouteDataProperties, } satisfies RouteDataProperties,
}), },
...extensionRefreshSwap(PasswordHistoryComponent, PasswordHistoryV2Component, { {
path: "cipher-password-history", path: "cipher-password-history",
component: PasswordHistoryV2Component,
canActivate: [authGuard], canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties, data: { elevation: 1 } satisfies RouteDataProperties,
}), },
...extensionRefreshSwap(AddEditComponent, AddEditV2Component, { {
path: "add-cipher", path: "add-cipher",
component: AddEditV2Component,
canActivate: [authGuard, debounceNavigationGuard()], canActivate: [authGuard, debounceNavigationGuard()],
data: { elevation: 1 } satisfies RouteDataProperties, data: { elevation: 1 } satisfies RouteDataProperties,
runGuardsAndResolvers: "always", runGuardsAndResolvers: "always",
}), },
...extensionRefreshSwap(AddEditComponent, AddEditV2Component, { {
path: "edit-cipher", path: "edit-cipher",
component: AddEditV2Component,
canActivate: [authGuard, debounceNavigationGuard()], canActivate: [authGuard, debounceNavigationGuard()],
data: { data: {
// Above "trash" // Above "trash"
elevation: 3, elevation: 3,
} satisfies RouteDataProperties, } satisfies RouteDataProperties,
runGuardsAndResolvers: "always", runGuardsAndResolvers: "always",
}),
{
path: "share-cipher",
component: ShareComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
}, },
{ {
path: "collections",
component: CollectionsComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
},
...extensionRefreshSwap(AttachmentsComponent, AttachmentsV2Component, {
path: "attachments", path: "attachments",
component: AttachmentsV2Component,
canActivate: [authGuard], canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties, data: { elevation: 1 } satisfies RouteDataProperties,
}), },
{ {
path: "generator", path: "generator",
component: CredentialGeneratorComponent, component: CredentialGeneratorComponent,
@@ -352,43 +326,28 @@ const routes: Routes = [
canActivate: [authGuard], canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties, data: { elevation: 1 } satisfies RouteDataProperties,
}), }),
...extensionRefreshSwap(AccountSecurityV1Component, AccountSecurityComponent, { {
path: "account-security", path: "account-security",
component: AccountSecurityComponent,
canActivate: [authGuard], canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties, data: { elevation: 1 } satisfies RouteDataProperties,
}), },
...extensionRefreshSwap(NotificationsSettingsV1Component, NotificationsSettingsComponent, { ...extensionRefreshSwap(NotificationsSettingsV1Component, NotificationsSettingsComponent, {
path: "notifications", path: "notifications",
canActivate: [authGuard], canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties, data: { elevation: 1 } satisfies RouteDataProperties,
}), }),
...extensionRefreshSwap(VaultSettingsComponent, VaultSettingsV2Component, { {
path: "vault-settings", path: "vault-settings",
component: VaultSettingsV2Component,
canActivate: [authGuard], canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties, data: { elevation: 1 } satisfies RouteDataProperties,
}), },
...extensionRefreshSwap(FoldersComponent, FoldersV2Component, { {
path: "folders", path: "folders",
component: FoldersV2Component,
canActivate: [authGuard], canActivate: [authGuard],
data: { elevation: 2 } satisfies RouteDataProperties, data: { elevation: 2 } satisfies RouteDataProperties,
}),
{
path: "add-folder",
component: FolderAddEditComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
},
{
path: "edit-folder",
component: FolderAddEditComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
},
{
path: "sync",
component: SyncComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
}, },
{ {
path: "blocked-domains", path: "blocked-domains",
@@ -407,16 +366,18 @@ const routes: Routes = [
canActivate: [authGuard], canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties, data: { elevation: 1 } satisfies RouteDataProperties,
}, },
...extensionRefreshSwap(AppearanceComponent, AppearanceV2Component, { {
path: "appearance", path: "appearance",
component: AppearanceV2Component,
canActivate: [authGuard], canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties, data: { elevation: 1 } satisfies RouteDataProperties,
}), },
...extensionRefreshSwap(AddEditComponent, AddEditV2Component, { {
path: "clone-cipher", path: "clone-cipher",
component: AddEditV2Component,
canActivate: [authGuard], canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties, data: { elevation: 1 } satisfies RouteDataProperties,
}), },
{ {
path: "add-send", path: "add-send",
component: SendAddEditV2Component, component: SendAddEditV2Component,
@@ -692,7 +653,7 @@ const routes: Routes = [
{ {
path: "assign-collections", path: "assign-collections",
component: AssignCollections, component: AssignCollections,
canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh, true, "/")], canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties, data: { elevation: 1 } satisfies RouteDataProperties,
}, },
{ {

View File

@@ -29,7 +29,6 @@ import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-req
import { RegisterComponent } from "../auth/popup/register.component"; import { RegisterComponent } from "../auth/popup/register.component";
import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
import { SetPasswordComponent } from "../auth/popup/set-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component";
import { AccountSecurityComponent as AccountSecurityComponentV1 } from "../auth/popup/settings/account-security-v1.component";
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component"; import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component";
import { SsoComponentV1 } from "../auth/popup/sso-v1.component"; import { SsoComponentV1 } from "../auth/popup/sso-v1.component";
@@ -55,25 +54,6 @@ import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.comp
import { PopupPageComponent } from "../platform/popup/layout/popup-page.component"; import { PopupPageComponent } from "../platform/popup/layout/popup-page.component";
import { PopupTabNavigationComponent } from "../platform/popup/layout/popup-tab-navigation.component"; import { PopupTabNavigationComponent } from "../platform/popup/layout/popup-tab-navigation.component";
import { FilePopoutCalloutComponent } from "../tools/popup/components/file-popout-callout.component"; import { FilePopoutCalloutComponent } from "../tools/popup/components/file-popout-callout.component";
import { ActionButtonsComponent } from "../vault/popup/components/action-buttons.component";
import { CipherRowComponent } from "../vault/popup/components/cipher-row.component";
import { AddEditCustomFieldsComponent } from "../vault/popup/components/vault/add-edit-custom-fields.component";
import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component";
import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component";
import { CollectionsComponent } from "../vault/popup/components/vault/collections.component";
import { CurrentTabComponent } from "../vault/popup/components/vault/current-tab.component";
import { PasswordHistoryComponent } from "../vault/popup/components/vault/password-history.component";
import { ShareComponent } from "../vault/popup/components/vault/share.component";
import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component";
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
import { VaultSelectComponent } from "../vault/popup/components/vault/vault-select.component";
import { ViewCustomFieldsComponent } from "../vault/popup/components/vault/view-custom-fields.component";
import { ViewComponent } from "../vault/popup/components/vault/view.component";
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
import { FoldersComponent } from "../vault/popup/settings/folders.component";
import { SyncComponent } from "../vault/popup/settings/sync.component";
import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component";
import { AppRoutingModule } from "./app-routing.module"; import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component"; import { AppComponent } from "./app.component";
@@ -128,49 +108,29 @@ import "../platform/popup/locales";
ExtensionAnonLayoutWrapperComponent, ExtensionAnonLayoutWrapperComponent,
], ],
declarations: [ declarations: [
ActionButtonsComponent,
AddEditComponent,
AddEditCustomFieldsComponent,
AppComponent, AppComponent,
AttachmentsComponent,
CipherRowComponent,
VaultItemsComponent,
CollectionsComponent,
ColorPasswordPipe, ColorPasswordPipe,
ColorPasswordCountPipe, ColorPasswordCountPipe,
CurrentTabComponent,
EnvironmentComponent, EnvironmentComponent,
ExcludedDomainsV1Component, ExcludedDomainsV1Component,
Fido2CipherRowV1Component, Fido2CipherRowV1Component,
Fido2UseBrowserLinkV1Component, Fido2UseBrowserLinkV1Component,
FolderAddEditComponent,
FoldersComponent,
VaultFilterComponent,
HintComponent, HintComponent,
HomeComponent, HomeComponent,
LoginViaAuthRequestComponentV1, LoginViaAuthRequestComponentV1,
LoginComponentV1, LoginComponentV1,
LoginDecryptionOptionsComponentV1, LoginDecryptionOptionsComponentV1,
NotificationsSettingsV1Component, NotificationsSettingsV1Component,
AppearanceComponent,
PasswordHistoryComponent,
RegisterComponent, RegisterComponent,
SetPasswordComponent, SetPasswordComponent,
VaultSettingsComponent,
ShareComponent,
SsoComponentV1, SsoComponentV1,
SyncComponent,
TabsV2Component, TabsV2Component,
TwoFactorComponent, TwoFactorComponent,
TwoFactorOptionsComponent, TwoFactorOptionsComponent,
UpdateTempPasswordComponent, UpdateTempPasswordComponent,
UserVerificationComponent, UserVerificationComponent,
AccountSecurityComponentV1,
VaultTimeoutInputComponent, VaultTimeoutInputComponent,
ViewComponent,
ViewCustomFieldsComponent,
RemovePasswordComponent, RemovePasswordComponent,
VaultSelectComponent,
Fido2V1Component, Fido2V1Component,
AutofillV1Component, AutofillV1Component,
EnvironmentSelectorComponent, EnvironmentSelectorComponent,

View File

@@ -1,7 +1,7 @@
import { inject } from "@angular/core"; import { inject } from "@angular/core";
import { CanDeactivateFn } from "@angular/router"; import { CanDeactivateFn } from "@angular/router";
import { VaultV2Component } from "../popup/components/vault/vault-v2.component"; import { VaultV2Component } from "../popup/components/vault-v2/vault-v2.component";
import { VaultPopupItemsService } from "../popup/services/vault-popup-items.service"; import { VaultPopupItemsService } from "../popup/services/vault-popup-items.service";
import { VaultPopupListFiltersService } from "../popup/services/vault-popup-list-filters.service"; import { VaultPopupListFiltersService } from "../popup/services/vault-popup-list-filters.service";

View File

@@ -1,102 +0,0 @@
<button
type="button"
class="row-btn"
(click)="view()"
appStopClick
appStopProp
appA11yTitle="{{ 'view' | i18n }}"
*ngIf="showView"
>
<i class="bwi bwi-lg bwi-list-alt" aria-hidden="true"></i>
</button>
<ng-container *ngIf="cipher.type === cipherType.Login">
<button
type="button"
class="row-btn"
appStopClick
appStopProp
appA11yTitle="{{ 'launch' | i18n }}"
(click)="launchCipher()"
*ngIf="!showView"
[ngClass]="{ disabled: !cipher.login.canLaunch }"
[attr.disabled]="!cipher.login.canLaunch ? '' : null"
>
<i class="bwi bwi-lg bwi-share-square" aria-hidden="true"></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appStopProp
appA11yTitle="{{ 'copyUsername' | i18n }}"
(click)="copy(cipher, cipher.login.username, 'username', 'Username')"
[ngClass]="{ disabled: !cipher.login.username }"
[attr.disabled]="!cipher.login.username ? '' : null"
>
<i class="bwi bwi-lg bwi-user" aria-hidden="true"></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appStopProp
appA11yTitle="{{ 'copyPassword' | i18n }}"
(click)="copy(cipher, cipher.login.password, 'password', 'Password')"
[ngClass]="{ disabled: !cipher.login.password || !cipher.viewPassword }"
[attr.disabled]="!cipher.login.password ? '' : null"
>
<i class="bwi bwi-lg bwi-key" aria-hidden="true"></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appStopProp
appA11yTitle="{{ 'copyVerificationCode' | i18n }}"
(click)="copy(cipher, cipher.login.totp, 'verificationCodeTotp', 'TOTP')"
[ngClass]="{ disabled: !displayTotpCopyButton(cipher) }"
[attr.disabled]="!displayTotpCopyButton(cipher) ? '' : null"
>
<i class="bwi bwi-lg bwi-clock" aria-hidden="true"></i>
</button>
</ng-container>
<ng-container *ngIf="cipher.type === cipherType.Card">
<button
type="button"
class="row-btn"
appStopClick
appStopProp
appA11yTitle="{{ 'copyNumber' | i18n }}"
(click)="copy(cipher, cipher.card.number, 'number', 'Card Number')"
[ngClass]="{ disabled: !cipher.card.number }"
[attr.disabled]="!cipher.card.number ? '' : null"
>
<i class="bwi bwi-lg bwi-hashtag" aria-hidden="true"></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appStopProp
appA11yTitle="{{ 'copySecurityCode' | i18n }}"
(click)="copy(cipher, cipher.card.code, 'securityCode', 'Security Code')"
[ngClass]="{ disabled: !cipher.card.code }"
[attr.disabled]="!cipher.card.code ? '' : null"
>
<i class="bwi bwi-lg bwi-key" aria-hidden="true"></i>
</button>
</ng-container>
<ng-container *ngIf="cipher.type === cipherType.SecureNote">
<button
type="button"
class="row-btn"
appStopClick
appStopProp
appA11yTitle="{{ 'copyNote' | i18n }}"
(click)="copy(cipher, cipher.notes, 'note', 'Note')"
[ngClass]="{ disabled: !cipher.notes }"
[attr.disabled]="!cipher.notes ? '' : null"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</ng-container>

View File

@@ -1,108 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { Subject, takeUntil } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { PasswordRepromptService } from "@bitwarden/vault";
@Component({
selector: "app-action-buttons",
templateUrl: "action-buttons.component.html",
})
export class ActionButtonsComponent implements OnInit, OnDestroy {
@Output() onView = new EventEmitter<CipherView>();
@Output() launchEvent = new EventEmitter<CipherView>();
@Input() cipher: CipherView;
@Input() showView = false;
cipherType = CipherType;
userHasPremiumAccess = false;
private componentIsDestroyed$ = new Subject<boolean>();
constructor(
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private eventCollectionService: EventCollectionService,
private totpService: TotpServiceAbstraction,
private passwordRepromptService: PasswordRepromptService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
) {}
ngOnInit() {
this.billingAccountProfileStateService.hasPremiumFromAnySource$
.pipe(takeUntil(this.componentIsDestroyed$))
.subscribe((canAccessPremium: boolean) => {
this.userHasPremiumAccess = canAccessPremium;
});
}
ngOnDestroy() {
this.componentIsDestroyed$.next(true);
this.componentIsDestroyed$.complete();
}
launchCipher() {
this.launchEvent.emit(this.cipher);
}
async copy(cipher: CipherView, value: string, typeI18nKey: string, aType: string) {
if (
this.cipher.reprompt !== CipherRepromptType.None &&
this.passwordRepromptService.protectedFields().includes(aType) &&
!(await this.passwordRepromptService.showPasswordPrompt())
) {
return;
}
if (value == null || (aType === "TOTP" && !this.displayTotpCopyButton(cipher))) {
return;
} else if (aType === "TOTP") {
value = await this.totpService.getCode(value);
}
if (!cipher.viewPassword) {
return;
}
this.platformUtilsService.copyToClipboard(value, { window: window });
this.platformUtilsService.showToast(
"info",
null,
this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)),
);
if (typeI18nKey === "password") {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id);
} else if (typeI18nKey === "verificationCodeTotp") {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedHiddenField, cipher.id);
} else if (typeI18nKey === "securityCode") {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedCardCode, cipher.id);
}
}
displayTotpCopyButton(cipher: CipherView) {
return (
(cipher?.login?.hasTotp ?? false) && (cipher.organizationUseTotp || this.userHasPremiumAccess)
);
}
view() {
this.onView.emit(this.cipher);
}
}

View File

@@ -1,51 +0,0 @@
<div
role="group"
appA11yTitle="{{ cipher.name }}"
class="virtual-scroll-item"
[ngClass]="{ 'override-last': !last }"
>
<div class="box-content-row box-content-row-flex">
<button
type="button"
(click)="selectCipher(cipher)"
(dblclick)="launchCipher(cipher)"
appStopClick
title="{{ title }} - {{ cipher.name }}"
class="row-main"
>
<app-vault-icon [cipher]="cipher"></app-vault-icon>
<div class="row-main-content">
<span class="text">
<span class="truncate-box">
<span class="truncate">{{ cipher.name }}</span>
<ng-container *ngIf="cipher.organizationId">
<i
class="bwi bwi-collection text-muted"
title="{{ 'shared' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "shared" | i18n }}</span>
</ng-container>
<ng-container *ngIf="cipher.hasAttachments">
<i
class="bwi bwi-paperclip text-muted"
title="{{ 'attachments' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "attachments" | i18n }}</span>
</ng-container>
</span>
</span>
<span class="detail">{{ cipher.subTitle }}</span>
</div>
</button>
<app-action-buttons
[cipher]="cipher"
[showView]="showView"
(onView)="viewCipher(cipher)"
(launchEvent)="launchCipher(cipher)"
class="action-buttons"
>
</app-action-buttons>
</div>
</div>

View File

@@ -1,31 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@Component({
selector: "app-cipher-row",
templateUrl: "cipher-row.component.html",
})
export class CipherRowComponent {
@Output() onSelected = new EventEmitter<CipherView>();
@Output() launchEvent = new EventEmitter<CipherView>();
@Output() onView = new EventEmitter<CipherView>();
@Input() cipher: CipherView;
@Input() last: boolean;
@Input() showView = false;
@Input() title: string;
selectCipher(c: CipherView) {
this.onSelected.emit(c);
}
launchCipher(c: CipherView) {
this.launchEvent.emit(c);
}
viewCipher(c: CipherView) {
this.onView.emit(c);
}
}

View File

@@ -39,7 +39,9 @@
" "
class="{{ itemHeightClass }}" class="{{ itemHeightClass }}"
> >
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon> <div slot="start" class="tw-justify-start tw-w-7 tw-flex">
<app-vault-icon [cipher]="cipher"></app-vault-icon>
</div>
<span data-testid="item-name">{{ cipher.name }}</span> <span data-testid="item-name">{{ cipher.name }}</span>
<i <i
*ngIf="cipher.organizationId" *ngIf="cipher.organizationId"

View File

@@ -1,4 +1,5 @@
<bit-search <bit-search
autocomplete="off"
[placeholder]="'search' | i18n" [placeholder]="'search' | i18n"
[(ngModel)]="searchText" [(ngModel)]="searchText"
(ngModelChange)="onSearchTextChanged()" (ngModelChange)="onSearchTextChanged()"

View File

@@ -19,12 +19,14 @@ import { VaultPopupAutofillService } from "../../services/vault-popup-autofill.s
import { VaultPopupItemsService } from "../../services/vault-popup-items.service"; import { VaultPopupItemsService } from "../../services/vault-popup-items.service";
import { VaultPopupListFiltersService } from "../../services/vault-popup-list-filters.service"; import { VaultPopupListFiltersService } from "../../services/vault-popup-list-filters.service";
import { VaultUiOnboardingService } from "../../services/vault-ui-onboarding.service"; import { VaultUiOnboardingService } from "../../services/vault-ui-onboarding.service";
import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "../vault-v2";
import { import {
NewItemDropdownV2Component, NewItemDropdownV2Component,
NewItemInitialValues, NewItemInitialValues,
} from "../vault-v2/new-item-dropdown/new-item-dropdown-v2.component"; } from "./new-item-dropdown/new-item-dropdown-v2.component";
import { VaultHeaderV2Component } from "../vault-v2/vault-header/vault-header-v2.component"; import { VaultHeaderV2Component } from "./vault-header/vault-header-v2.component";
import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from ".";
enum VaultState { enum VaultState {
Empty, Empty,

View File

@@ -1,140 +0,0 @@
<div class="box">
<h2 class="box-header">
{{ "customFields" | i18n }}
</h2>
<div class="box-content">
<!-- Current custom fields -->
<div cdkDropList (cdkDropListDropped)="drop($event)" *ngIf="cipher.hasFields">
<div
role="group"
class="box-content-row box-content-row-multi box-draggable-row"
appBoxRow
cdkDrag
*ngFor="let f of cipher.fields; let i = index; trackBy: trackByFunction"
[ngClass]="{ 'box-content-row-checkbox': f.type === fieldType.Boolean }"
attr.aria-label="{{ f.name }}"
>
<button
type="button"
appStopClick
(click)="removeField(f)"
appA11yTitle="{{ 'remove' | i18n }}"
*ngIf="!(!cipher.edit && editMode)"
>
<i class="bwi bwi-minus-circle bwi-lg" aria-hidden="true"></i>
</button>
<label for="fieldName{{ i }}" class="sr-only">{{ "name" | i18n }}</label>
<label for="fieldValue{{ i }}" class="sr-only">{{ "value" | i18n }}</label>
<div class="row-main">
<input
id="fieldName{{ i }}"
type="text"
name="Field.Name{{ i }}"
[(ngModel)]="f.name"
class="row-label"
placeholder="{{ 'name' | i18n }}"
appInputVerbatim
[readonly]="!cipher.edit && editMode"
/>
<!-- Text -->
<input
id="fieldValue{{ i }}"
type="text"
name="Field.Value{{ i }}"
[(ngModel)]="f.value"
*ngIf="f.type === fieldType.Text"
placeholder="{{ 'value' | i18n }}"
appInputVerbatim
attr.aria-describedby="fieldName{{ i }}"
[readonly]="!cipher.edit && editMode"
/>
<!-- Hidden -->
<input
id="fieldValue{{ i }}"
type="{{ f.showValue ? 'text' : 'password' }}"
name="Field.Value{{ i }}"
[(ngModel)]="f.value"
class="monospaced"
appInputVerbatim
*ngIf="f.type === fieldType.Hidden"
placeholder="{{ 'value' | i18n }}"
[disabled]="!cipher.viewPassword && !f.newField"
attr.aria-describedby="fieldName{{ i }}"
[readonly]="!cipher.edit && editMode"
/>
<!-- Linked -->
<select
id="fieldValue{{ i }}"
name="Field.Value{{ i }}"
[(ngModel)]="f.linkedId"
*ngIf="f.type === fieldType.Linked && cipher.linkedFieldOptions != null"
attr.aria-describedby="fieldName{{ i }}"
>
<option *ngFor="let o of linkedFieldOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
<!-- Boolean -->
<input
id="fieldValue{{ i }}"
name="Field.Value{{ i }}"
type="checkbox"
[(ngModel)]="f.value"
*ngIf="f.type === fieldType.Boolean"
appTrueFalseValue
trueValue="true"
falseValue="false"
attr.aria-describedby="fieldName{{ i }}"
[readonly]="!cipher.edit && editMode"
/>
<div
class="action-buttons"
*ngIf="f.type === fieldType.Hidden && (cipher.viewPassword || f.newField)"
>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleFieldValue(f)"
[attr.aria-pressed]="f.showValue"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !f.showValue, 'bwi-eye-slash': f.showValue }"
></i>
</button>
</div>
<div
class="drag-handle"
appA11yTitle="{{ 'dragToSort' | i18n }}"
*ngIf="!(!cipher.edit && editMode)"
cdkDragHandle
>
<i class="bwi bwi-hamburger" aria-hidden="true"></i>
</div>
</div>
</div>
<!-- Add new custom field -->
<div
class="box-content-row box-content-row-newmulti"
*ngIf="!(!cipher.edit && editMode)"
appBoxRow
>
<button type="button" appStopClick (click)="addField()">
<i class="bwi bwi-plus-circle bwi-fw bwi-lg" aria-hidden="true"></i>
{{ "newCustomField" | i18n }}
</button>
<label for="addFieldType" class="sr-only">{{ "type" | i18n }}</label>
<select id="addFieldType" name="AddFieldType" [(ngModel)]="addFieldType" class="field-type">
<option *ngFor="let o of addFieldTypeOptions" [ngValue]="o.value">{{ o.name }}</option>
<option
*ngIf="cipher.linkedFieldOptions != null"
[ngValue]="addFieldLinkedTypeOption.value"
>
{{ addFieldLinkedTypeOption.name }}
</option>
</select>
</div>
</div>
</div>

View File

@@ -1,15 +0,0 @@
import { Component } from "@angular/core";
import { AddEditCustomFieldsComponent as BaseAddEditCustomFieldsComponent } from "@bitwarden/angular/vault/components/add-edit-custom-fields.component";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@Component({
selector: "app-vault-add-edit-custom-fields",
templateUrl: "add-edit-custom-fields.component.html",
})
export class AddEditCustomFieldsComponent extends BaseAddEditCustomFieldsComponent {
constructor(i18nService: I18nService, eventCollectionService: EventCollectionService) {
super(i18nService, eventCollectionService);
}
}

View File

@@ -1,826 +0,0 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<header>
<div class="left">
<button type="button" (click)="cancel()">{{ "cancel" | i18n }}</button>
</div>
<h1 class="center">
<span class="title">{{ title }}</span>
</h1>
<div class="right">
<button type="submit" [disabled]="form.loading">
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
</div>
</header>
<main tabindex="-1" *ngIf="cipher">
<app-callout type="info" *ngIf="allowOwnershipOptions() && !allowPersonal">
{{ "personalOwnershipPolicyInEffect" | i18n }}
</app-callout>
<div class="box">
<h2 class="box-header">
{{ "itemInformation" | i18n }}
</h2>
<div class="box-content">
<div class="box-content-row" *ngIf="!editMode" appBoxRow>
<label for="type">{{ "type" | i18n }}</label>
<select id="type" name="Type" [(ngModel)]="cipher.type">
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
<div class="box-content-row" appBoxRow>
<label for="name">{{ "name" | i18n }}</label>
<input
id="name"
type="text"
name="Name"
[(ngModel)]="cipher.name"
[readonly]="!cipher.edit && editMode"
/>
</div>
<!-- Login -->
<div *ngIf="cipher.type === cipherType.Login">
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="loginUsername">{{ "username" | i18n }}</label>
<input
id="loginUsername"
type="text"
name="Login.Username"
[(ngModel)]="cipher.login.username"
inputmode="email"
appInputVerbatim
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'generateUsername' | i18n }}"
(click)="generateUsername()"
*ngIf="!(!cipher.edit && editMode)"
>
<i class="bwi bwi-fw bwi-lg bwi-generate" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="loginPassword">{{ "password" | i18n }}</label>
<input
id="loginPassword"
class="monospaced"
type="{{ showPassword ? 'text' : 'password' }}"
name="Login.Password"
[(ngModel)]="cipher.login.password"
appInputVerbatim
[disabled]="!cipher.viewPassword"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="action-buttons">
<button
type="button"
#checkPasswordBtn
class="row-btn btn"
appA11yTitle="{{ 'checkPassword' | i18n }}"
(click)="checkPassword()"
[appApiAction]="checkPasswordPromise"
[disabled]="$any(checkPasswordBtn).loading"
*ngIf="cipher.viewPassword"
>
<i
class="bwi bwi-fw bwi-lg bwi-check-circle"
[hidden]="$any(checkPasswordBtn).loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-fw bwi-lg bwi-spinner bwi-spin"
[hidden]="!$any(checkPasswordBtn).loading"
aria-hidden="true"
></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword()"
*ngIf="cipher.viewPassword && cipher.login.password"
[attr.aria-pressed]="showPassword"
>
<i
class="bwi bwi-fw bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'generatePassword' | i18n }}"
(click)="generatePassword()"
*ngIf="cipher.viewPassword && !(!cipher.edit && editMode)"
>
<i class="bwi bwi-fw bwi-lg bwi-generate" aria-hidden="true"></i>
</button>
</div>
</div>
<!--Passkey-->
<div
class="box"
*ngIf="cipher.login.hasFido2Credentials && !cloneMode"
tabindex="0"
attr.aria-label="{{ 'typePasskey' | i18n }} {{ fido2CredentialCreationDateValue }}"
>
<div class="box-content">
<div class="box-content-row box-content-row-multi text-muted" appBoxRow>
<button
type="button"
appStopClick
(click)="removePasskey()"
appA11yTitle="{{ 'removePasskey' | i18n }}"
*ngIf="!(!cipher.edit && editMode)"
>
<i class="bwi bwi-fw bwi-minus-circle bwi-lg" aria-hidden="true"></i>
</button>
<div class="row-main">
<span class="row-label">{{ "typePasskey" | i18n }}</span>
{{ "dateCreated" | i18n }}
{{ cipher.login.fido2Credentials[0].creationDate | date: "short" }}
</div>
</div>
</div>
</div>
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="loginTotp">{{ "authenticatorKeyTotp" | i18n }}</label>
<input
id="loginTotp"
type="{{ showTotpSeed ? 'text' : 'password' }}"
name="Login.Totp"
class="monospaced"
[(ngModel)]="cipher.login.totp"
appInputVerbatim
[disabled]="!cipher.viewPassword"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleTotpSeed()"
*ngIf="cipher.viewPassword && cipher.login.totp"
[attr.aria-pressed]="showTotpSeed"
>
<i
class="bwi bwi-fw bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showTotpSeed, 'bwi-eye-slash': showTotpSeed }"
></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'copyTOTP' | i18n }}"
(click)="copy(cipher.login.totp, 'totp', 'TOTP')"
*ngIf="cipher.viewPassword"
>
<i class="bwi bwi-fw bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'totpCapture' | i18n }}"
(click)="captureTOTPFromTab()"
*ngIf="!(!cipher.edit && editMode)"
>
<i class="bwi bwi-fw bwi-lg bwi-camera" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<!-- Card -->
<div *ngIf="cipher.type === cipherType.Card">
<div class="box-content-row" appBoxRow>
<label for="cardCardholderName">{{ "cardholderName" | i18n }}</label>
<input
id="cardCardholderName"
type="text"
name="Card.CardCardholderName"
[(ngModel)]="cipher.card.cardholderName"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="cardNumber">{{ "number" | i18n }}</label>
<input
id="cardNumber"
class="monospaced"
type="{{ showCardNumber ? 'text' : 'password' }}"
name="Card.Number"
(input)="onCardNumberChange()"
[(ngModel)]="cipher.card.number"
appInputVerbatim
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleCardNumber()"
[attr.aria-pressed]="showCardNumber"
>
<i
class="bwi bwi-fw bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showCardNumber, 'bwi-eye-slash': showCardNumber }"
></i>
</button>
</div>
</div>
<div class="box-content-row" appBoxRow>
<label for="cardBrand">{{ "brand" | i18n }}</label>
<span *ngIf="!(!cipher.edit && editMode); else readonlyCardBrand">
<select id="cardBrand" name="Card.Brand" [(ngModel)]="cipher.card.brand">
<option *ngFor="let o of cardBrandOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</span>
<ng-template #readonlyCardBrand>
<input
id="cardBrand"
name="Card.Brand"
type="text"
[readonly]="true"
[value]="cipher.card.brand"
/>
</ng-template>
</div>
<div class="box-content-row" appBoxRow>
<label for="cardExpMonth">{{ "expirationMonth" | i18n }}</label>
<span *ngIf="!(!cipher.edit && editMode); else readonlyCardExpMonth">
<select id="cardExpMonth" name="Card.ExpMonth" [(ngModel)]="cipher.card.expMonth">
<option *ngFor="let o of cardExpMonthOptions" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</span>
<ng-template #readonlyCardExpMonth>
<input
id="cardExpMonth"
name="Card.ExpMonth"
type="text"
[readonly]="true"
[value]="getCardExpMonthDisplay()"
/>
</ng-template>
</div>
<div class="box-content-row" appBoxRow>
<label for="cardExpYear">{{ "expirationYear" | i18n }}</label>
<input
id="cardExpYear"
type="text"
name="Card.ExpYear"
[(ngModel)]="cipher.card.expYear"
placeholder="{{ 'ex' | i18n }} {{ currentDate | date: 'yyyy' }}"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="cardCode">{{ "securityCode" | i18n }}</label>
<input
id="cardCode"
class="monospaced"
type="{{ showCardCode ? 'text' : 'password' }}"
name="Card.Code"
[(ngModel)]="cipher.card.code"
appInputVerbatim
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleCardCode()"
[attr.aria-pressed]="showCardCode"
>
<i
class="bwi bwi-fw bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showCardCode, 'bwi-eye-slash': showCardCode }"
></i>
</button>
</div>
</div>
</div>
<!-- Identity -->
<div *ngIf="cipher.type === cipherType.Identity">
<div class="box-content-row" appBoxRow>
<label for="idTitle">{{ "title" | i18n }}</label>
<span *ngIf="!(!cipher.edit && editMode); else readonlyIdTitle">
<select id="idTitle" name="Identity.Title" [(ngModel)]="cipher.identity.title">
<option *ngFor="let o of identityTitleOptions" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</span>
<ng-template #readonlyIdTitle>
<input
id="idTitle"
name="Identity.Title"
type="text"
[readonly]="true"
[value]="cipher.identity.title"
/>
</ng-template>
</div>
<div class="box-content-row" appBoxRow>
<label for="idFirstName">{{ "firstName" | i18n }}</label>
<input
id="idFirstName"
type="text"
name="Identity.FirstName"
[(ngModel)]="cipher.identity.firstName"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idMiddleName">{{ "middleName" | i18n }}</label>
<input
id="idMiddleName"
type="text"
name="Identity.MiddleName"
[(ngModel)]="cipher.identity.middleName"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idLastName">{{ "lastName" | i18n }}</label>
<input
id="idLastName"
type="text"
name="Identity.LastName"
[(ngModel)]="cipher.identity.lastName"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idUsername">{{ "username" | i18n }}</label>
<input
id="idUsername"
type="text"
name="Identity.Username"
[(ngModel)]="cipher.identity.username"
appInputVerbatim
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idCompany">{{ "company" | i18n }}</label>
<input
id="idCompany"
type="text"
name="Identity.Company"
[(ngModel)]="cipher.identity.company"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idSsn">{{ "ssn" | i18n }}</label>
<input
id="idSsn"
type="text"
name="Identity.SSN"
[(ngModel)]="cipher.identity.ssn"
appInputVerbatim
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idPassportNumber">{{ "passportNumber" | i18n }}</label>
<input
id="idPassportNumber"
type="text"
name="Identity.PassportNumber"
[(ngModel)]="cipher.identity.passportNumber"
appInputVerbatim
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idLicenseNumber">{{ "licenseNumber" | i18n }}</label>
<input
id="idLicenseNumber"
type="text"
name="Identity.LicenseNumber"
[(ngModel)]="cipher.identity.licenseNumber"
appInputVerbatim
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idEmail">{{ "email" | i18n }}</label>
<input
id="idEmail"
type="text"
name="Identity.Email"
[(ngModel)]="cipher.identity.email"
appInputVerbatim
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idPhone">{{ "phone" | i18n }}</label>
<input
id="idPhone"
type="text"
name="Identity.Phone"
[(ngModel)]="cipher.identity.phone"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idAddress1">{{ "address1" | i18n }}</label>
<input
id="idAddress1"
type="text"
name="Identity.Address1"
[(ngModel)]="cipher.identity.address1"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idAddress2">{{ "address2" | i18n }}</label>
<input
id="idAddress2"
type="text"
name="Identity.Address2"
[(ngModel)]="cipher.identity.address2"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idAddress3">{{ "address3" | i18n }}</label>
<input
id="idAddress3"
type="text"
name="Identity.Address3"
[(ngModel)]="cipher.identity.address3"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idCity">{{ "cityTown" | i18n }}</label>
<input
id="idCity"
type="text"
name="Identity.City"
[(ngModel)]="cipher.identity.city"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idState">{{ "stateProvince" | i18n }}</label>
<input
id="idState"
type="text"
name="Identity.State"
[(ngModel)]="cipher.identity.state"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idPostalCode">{{ "zipPostalCode" | i18n }}</label>
<input
id="idPostalCode"
type="text"
name="Identity.PostalCode"
[(ngModel)]="cipher.identity.postalCode"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idCountry">{{ "country" | i18n }}</label>
<input
id="idCountry"
type="text"
name="Identity.Country"
[(ngModel)]="cipher.identity.country"
[readonly]="!cipher.edit && editMode"
/>
</div>
</div>
<!-- SshKey -->
<div *ngIf="cipher.sshKey">
<div class="box-content-row" *ngIf="cipher.sshKey.privateKey" style="overflow: hidden">
<span class="row-label"> {{ "sshPrivateKey" | i18n }}</span>
{{ cipher.sshKey.privateKey }}
</div>
<div class="box-content-row" *ngIf="cipher.sshKey.publicKey" style="overflow: hidden">
<span class="row-label"> {{ "sshPublicKey" | i18n }}</span>
{{ cipher.sshKey.publicKey }}
</div>
<div
class="box-content-row"
*ngIf="cipher.sshKey.keyFingerprint"
style="overflow: hidden"
>
<span class="row-label"> {{ "sshKeyFingerprint" | i18n }}</span>
{{ cipher.sshKey.keyFingerprint }}
</div>
</div>
</div>
</div>
<div class="box" *ngIf="cipher.type === cipherType.Login">
<div class="box-content">
<ng-container *ngIf="cipher.login.hasUris">
<div
role="group"
class="box-content-row box-content-row-multi"
appBoxRow
*ngFor="let u of cipher.login.uris; let i = index; trackBy: trackByFunction"
attr.aria-label="{{ 'uriPosition' | i18n: i + 1 }}"
>
<button
type="button"
*ngIf="!(!cipher.edit && editMode)"
appStopClick
(click)="removeUri(u)"
appA11yTitle="{{ 'remove' | i18n }}"
>
<i class="bwi bwi-fw bwi-minus-circle bwi-lg" aria-hidden="true"></i>
</button>
<div class="row-main">
<label for="loginUri{{ i }}">{{ "uriPosition" | i18n: i + 1 }}</label>
<input
id="loginUri{{ i }}"
type="text"
name="Login.Uris[{{ i }}].Uri"
[(ngModel)]="u.uri"
[hidden]="$any(u).showUriOptionsInput === true"
placeholder="{{ 'ex' | i18n }} https://google.com"
inputmode="url"
[readonly]="!cipher.edit && editMode"
appInputVerbatim
/>
<label for="loginUriMatch{{ i }}" class="sr-only">
{{ "currentUri" | i18n }} {{ i + 1 }}
</label>
<select
*ngIf="currentUris && currentUris.length"
id="currentUris{{ i }}"
name="Login.Uris[{{ i }}].CurrentUris"
[(ngModel)]="u.uri"
[hidden]="!$any(u).showCurrentUris"
>
<option [ngValue]="null">-- {{ "select" | i18n }} --</option>
<option *ngFor="let u of currentUris" [ngValue]="u">{{ u }}</option>
</select>
<label for="loginUriMatch{{ i }}" class="sr-only">
{{ "matchDetection" | i18n }} {{ i + 1 }}
</label>
<select
id="loginUriMatch{{ i }}"
name="Login.Uris[{{ i }}].Match"
[(ngModel)]="u.match"
[hidden]="
$any(u).showOptions === false || ($any(u).showOptions == null && u.match == null)
"
(change)="loginUriMatchChanged(u)"
>
<option *ngFor="let o of uriMatchOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
<div class="action-buttons">
<button
type="button"
*ngIf="currentUris && currentUris.length"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleCurrentUris' | i18n }}"
(click)="toggleUriInput(u)"
[attr.aria-pressed]="$any(u).showCurrentUris === true"
>
<i aria-hidden="true" class="bwi bwi-fw bwi-lg bwi-list"></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleOptions' | i18n }}"
(click)="toggleUriOptions(u)"
[attr.aria-pressed]="$any(u).showOptions === true"
[disabled]="!cipher.edit && editMode"
>
<i class="bwi bwi-fw bwi-lg bwi-cog" aria-hidden="true"></i>
</button>
</div>
</div>
</ng-container>
<button
type="button"
appStopClick
(click)="addUri()"
class="box-content-row box-content-row-newmulti single-line"
*ngIf="!(!cipher.edit && editMode)"
>
<i class="bwi bwi-plus-circle bwi-fw bwi-lg" aria-hidden="true"></i> {{ "newUri" | i18n }}
</button>
</div>
</div>
<div class="box" *ngIf="showAutoFillOnPageLoadOptions">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="autofillOnPageLoad">{{ "itemAutoFillOnPageLoad" | i18n }} </label>
<select
id="autofillOnPageLoad"
name="AutofillOnPageLoad"
[disabled]="reprompt"
[(ngModel)]="cipher.login.autofillOnPageLoad"
>
<option *ngFor="let o of autofillOnPageLoadOptions" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</div>
</div>
<div class="box-footer !tw-mb-0 !tw-pb-0" *ngIf="reprompt">
{{ "turnOffMasterPasswordPromptToEditField" | i18n }}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="folder">{{ "folder" | i18n }}</label>
<select id="folder" name="FolderId" [(ngModel)]="cipher.folderId">
<option *ngFor="let f of folders$ | async" [ngValue]="f.id">{{ f.name }}</option>
</select>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="favorite">{{ "favorite" | i18n }}</label>
<input id="favorite" type="checkbox" name="Favorite" [(ngModel)]="cipher.favorite" />
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow *ngIf="canUseReprompt">
<label for="passwordPrompt">
{{ "passwordPrompt" | i18n }}
<a
target="_blank"
rel="noreferrer"
appA11yTitle="{{ 'learnMore' | i18n }}"
href="https://bitwarden.com/help/managing-items/#protect-individual-items"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</label>
<input
id="passwordPrompt"
type="checkbox"
name="PasswordPrompt"
[ngModel]="reprompt"
(change)="repromptChanged()"
[disabled]="!cipher.edit && editMode"
/>
</div>
<button
type="button"
class="box-content-row box-content-row-flex text-default single-line"
appStopClick
(click)="attachments()"
*ngIf="editMode && showAttachments && !cloneMode"
>
<div class="row-main">{{ "attachments" | i18n }}</div>
<i
class="bwi bwi-external-link bwi-lg bwi-fw"
aria-hidden="true"
*ngIf="openAttachmentsInPopup"
></i>
<i
class="bwi bwi-angle-right row-sub-icon"
aria-hidden="true"
*ngIf="!openAttachmentsInPopup"
></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="editCollections()"
*ngIf="editMode && cipher.organizationId && !cloneMode"
>
<div class="row-main">{{ "collections" | i18n }}</div>
<i class="bwi bwi-angle-right row-sub-icon" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="box">
<h2 class="box-header">
<label for="notes">{{ "notes" | i18n }}</label>
</h2>
<div class="box-content">
<div class="box-content-row" appBoxRow>
<textarea
id="notes"
name="Notes"
rows="6"
[(ngModel)]="cipher.notes"
[readonly]="!cipher.edit && editMode"
></textarea>
</div>
</div>
</div>
<app-vault-add-edit-custom-fields
*ngIf="!(!cipher.hasFields && !cipher.edit && editMode)"
[cipher]="cipher"
[thisCipherType]="cipher.type"
[editMode]="editMode"
>
</app-vault-add-edit-custom-fields>
<div class="box" *ngIf="allowOwnershipOptions()">
<h2 class="box-header">
{{ "ownership" | i18n }}
</h2>
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="organizationId">{{ "whoOwnsThisItem" | i18n }}</label>
<select
id="organizationId"
class="form-control"
name="OrganizationId"
[(ngModel)]="cipher.organizationId"
(change)="organizationChanged()"
>
<option *ngFor="let o of ownershipOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
</div>
</div>
<div class="box" *ngIf="(!editMode || cloneMode) && cipher.organizationId">
<h2 class="box-header">
{{ "collections" | i18n }}
</h2>
<div class="box-content" *ngIf="!collections || !collections.length">
<div class="box-content-row padded no-hover">
{{ "noCollectionsInList" | i18n }}
</div>
</div>
<div class="box-content" *ngIf="collections && collections.length">
<div
class="box-content-row box-content-row-checkbox"
*ngFor="let c of collections; let i = index"
appBoxRow
>
<label for="collection_{{ i }}">{{ c.name }}</label>
<input
id="collection_{{ i }}"
type="checkbox"
[(ngModel)]="$any(c).checked"
name="Collection[{{ i }}].Checked"
/>
</div>
</div>
</div>
<div class="box list" *ngIf="editMode && !cloneMode && (canDeleteCipher$ | async)">
<div class="box-content single-line">
<button
type="button"
class="box-content-row"
appStopClick
(click)="delete()"
[appApiAction]="deletePromise"
#deleteBtn
>
<div class="row-main text-danger">
<div class="icon text-danger" aria-hidden="true">
<i class="bwi bwi-trash bwi-lg bwi-fw" [hidden]="$any(deleteBtn).loading"></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!$any(deleteBtn).loading"
></i>
</div>
<span>{{ "deleteItem" | i18n }}</span>
</div>
</button>
</div>
</div>
</main>
</form>

View File

@@ -1,417 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DatePipe, Location } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import qrcodeParser from "qrcode-parser";
import { firstValueFrom } from "rxjs";
import { first } from "rxjs/operators";
import { CollectionService } from "@bitwarden/admin-console/common";
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService } from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
import { BrowserFido2UserInterfaceSession } from "../../../../autofill/fido2/services/browser-fido2-user-interface.service";
import { BrowserApi } from "../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
import { PopupCloseWarningService } from "../../../../popup/services/popup-close-warning.service";
import { Fido2UserVerificationService } from "../../../services/fido2-user-verification.service";
import { fido2PopoutSessionData$ } from "../../utils/fido2-popout-session-data";
import { closeAddEditVaultItemPopout, VaultPopoutType } from "../../utils/vault-popout-window";
@Component({
selector: "app-vault-add-edit",
templateUrl: "add-edit.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class AddEditComponent extends BaseAddEditComponent implements OnInit {
currentUris: string[];
showAttachments = true;
openAttachmentsInPopup: boolean;
showAutoFillOnPageLoadOptions: boolean;
private fido2PopoutSessionData$ = fido2PopoutSessionData$();
constructor(
cipherService: CipherService,
folderService: FolderService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
auditService: AuditService,
accountService: AccountService,
private autofillSettingsService: AutofillSettingsServiceAbstraction,
collectionService: CollectionService,
messagingService: MessagingService,
private route: ActivatedRoute,
private router: Router,
private location: Location,
eventCollectionService: EventCollectionService,
policyService: PolicyService,
private popupCloseWarningService: PopupCloseWarningService,
organizationService: OrganizationService,
passwordRepromptService: PasswordRepromptService,
logService: LogService,
dialogService: DialogService,
datePipe: DatePipe,
configService: ConfigService,
private fido2UserVerificationService: Fido2UserVerificationService,
cipherAuthorizationService: CipherAuthorizationService,
) {
super(
cipherService,
folderService,
i18nService,
platformUtilsService,
auditService,
accountService,
collectionService,
messagingService,
eventCollectionService,
policyService,
logService,
passwordRepromptService,
organizationService,
dialogService,
window,
datePipe,
configService,
cipherAuthorizationService,
);
}
async ngOnInit() {
await super.ngOnInit();
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {
if (params.cipherId) {
this.cipherId = params.cipherId;
}
if (params.folderId) {
this.folderId = params.folderId;
}
if (params.collectionId) {
this.collectionId = params.collectionId;
const collection = this.writeableCollections.find((c) => c.id === params.collectionId);
if (collection != null) {
this.collectionIds = [collection.id];
this.organizationId = collection.organizationId;
}
}
if (params.type) {
const type = parseInt(params.type, null);
this.type = type;
}
this.editMode = !params.cipherId;
if (params.cloneMode != null) {
this.cloneMode = params.cloneMode === "true";
}
if (params.selectedVault) {
this.organizationId = params.selectedVault;
}
await this.load();
if (!this.editMode || this.cloneMode) {
// Only allow setting username if there's no existing value
if (
params.username &&
(this.cipher.login.username == null || this.cipher.login.username === "")
) {
this.cipher.login.username = params.username;
}
if (params.name && (this.cipher.name == null || this.cipher.name === "")) {
this.cipher.name = params.name;
}
if (
params.uri &&
this.cipher.login.uris[0] &&
(this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === "")
) {
this.cipher.login.uris[0].uri = params.uri;
}
}
this.openAttachmentsInPopup = BrowserPopupUtils.inPopup(window);
if (this.inAddEditPopoutWindow()) {
BrowserApi.messageListener("add-edit-popout", this.handleExtensionMessage.bind(this));
}
});
if (!this.editMode) {
const tabs = await BrowserApi.tabsQuery({ windowType: "normal" });
this.currentUris =
tabs == null
? null
: tabs.filter((tab) => tab.url != null && tab.url !== "").map((tab) => tab.url);
}
this.setFocus();
if (BrowserPopupUtils.inPopout(window)) {
this.popupCloseWarningService.enable();
}
}
async load() {
await super.load();
this.showAutoFillOnPageLoadOptions =
this.cipher.type === CipherType.Login &&
(await firstValueFrom(this.autofillSettingsService.autofillOnPageLoad$));
}
async submit(): Promise<boolean> {
const fido2SessionData = await firstValueFrom(this.fido2PopoutSessionData$);
const { isFido2Session, sessionId, userVerification } = fido2SessionData;
const inFido2PopoutWindow = BrowserPopupUtils.inPopout(window) && isFido2Session;
// normalize card expiry year on save
if (this.cipher.type === this.cipherType.Card) {
this.cipher.card.expYear = normalizeExpiryYearFormat(this.cipher.card.expYear);
}
// TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production.
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
if (
inFido2PopoutWindow &&
!(await this.handleFido2UserVerification(sessionId, userVerification))
) {
return false;
}
const success = await super.submit();
if (!success) {
return false;
}
if (BrowserPopupUtils.inPopout(window)) {
this.popupCloseWarningService.disable();
}
if (inFido2PopoutWindow) {
BrowserFido2UserInterfaceSession.confirmNewCredentialResponse(
sessionId,
this.cipher.id,
userVerification,
);
return true;
}
if (this.inAddEditPopoutWindow()) {
this.messagingService.send("addEditCipherSubmitted");
await closeAddEditVaultItemPopout(1000);
return true;
}
if (this.cloneMode) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/tabs/vault"]);
} else {
this.location.back();
}
return true;
}
attachments() {
super.attachments();
if (this.openAttachmentsInPopup) {
const destinationUrl = this.router
.createUrlTree(["/attachments"], { queryParams: { cipherId: this.cipher.id } })
.toString();
const currentBaseUrl = window.location.href.replace(this.router.url, "");
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserPopupUtils.openCurrentPagePopout(window, currentBaseUrl + destinationUrl);
} else {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/attachments"], { queryParams: { cipherId: this.cipher.id } });
}
}
editCollections() {
super.editCollections();
if (this.cipher.organizationId != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/collections"], { queryParams: { cipherId: this.cipher.id } });
}
}
async cancel() {
super.cancel();
const sessionData = await firstValueFrom(this.fido2PopoutSessionData$);
if (BrowserPopupUtils.inPopout(window) && sessionData.isFido2Session) {
this.popupCloseWarningService.disable();
BrowserFido2UserInterfaceSession.abortPopout(sessionData.sessionId);
return;
}
if (this.inAddEditPopoutWindow()) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
closeAddEditVaultItemPopout();
return;
}
this.location.back();
}
async generateUsername(): Promise<boolean> {
const confirmed = await super.generateUsername();
if (confirmed) {
await this.saveCipherState();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["generator"], { queryParams: { type: "username" } });
}
return confirmed;
}
async generatePassword(): Promise<boolean> {
const confirmed = await super.generatePassword();
if (confirmed) {
await this.saveCipherState();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["generator"], { queryParams: { type: "password" } });
}
return confirmed;
}
async delete(): Promise<boolean> {
const confirmed = await super.delete();
if (confirmed) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/tabs/vault"]);
}
return confirmed;
}
toggleUriInput(uri: LoginUriView) {
const u = uri as any;
u.showCurrentUris = !u.showCurrentUris;
}
allowOwnershipOptions(): boolean {
return (
(!this.editMode || this.cloneMode) &&
this.ownershipOptions &&
(this.ownershipOptions.length > 1 || !this.allowPersonal)
);
}
private saveCipherState() {
return this.cipherService.setAddEditCipherInfo({
cipher: this.cipher,
collectionIds:
this.collections == null
? []
: this.collections.filter((c) => (c as any).checked).map((c) => c.id),
});
}
private setFocus() {
window.setTimeout(() => {
if (this.editMode) {
return;
}
if (this.cipher.name != null && this.cipher.name !== "") {
document.getElementById("loginUsername").focus();
} else {
document.getElementById("name").focus();
}
}, 200);
}
repromptChanged() {
super.repromptChanged();
if (!this.showAutoFillOnPageLoadOptions) {
return;
}
if (this.reprompt) {
this.platformUtilsService.showToast(
"info",
null,
this.i18nService.t("passwordRepromptDisabledAutofillOnPageLoad"),
);
return;
}
this.platformUtilsService.showToast(
"info",
null,
this.i18nService.t("autofillOnPageLoadSetToDefault"),
);
}
private inAddEditPopoutWindow() {
return BrowserPopupUtils.inSingleActionPopout(window, VaultPopoutType.addEditVaultItem);
}
async captureTOTPFromTab() {
try {
const screenshot = await BrowserApi.captureVisibleTab();
const data = await qrcodeParser(screenshot);
const url = new URL(data.toString());
if (url.protocol == "otpauth:" && url.searchParams.has("secret")) {
this.cipher.login.totp = data.toString();
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("totpCaptureSuccess"),
);
}
} catch (e) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("totpCaptureError"),
);
}
}
private handleExtensionMessage(message: { [key: string]: any; command: string }) {
if (message.command === "inlineAutofillMenuRefreshAddEditCipher") {
this.load().catch((error) => this.logService.error(error));
}
}
// TODO: Remove and use fido2 user verification service once user verification for passkeys is approved for production.
// Be sure to make the same changes to add-edit-v2.component.ts if applicable
private async handleFido2UserVerification(
sessionId: string,
userVerification: boolean,
): Promise<boolean> {
// We are bypassing user verification pending approval for production.
return true;
}
}

View File

@@ -1,72 +0,0 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<header>
<div class="left">
<button type="button" (click)="close()" *ngIf="openedAttachmentsInPopup">
{{ "close" | i18n }}
</button>
<button type="button" (click)="back()" *ngIf="!openedAttachmentsInPopup">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "attachments" | i18n }}</span>
</h1>
<div class="right">
<button type="submit" [disabled]="form.loading">
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
</div>
</header>
<main tabindex="-1">
<div class="box" *ngIf="cipher && cipher.hasAttachments">
<div class="box-content no-hover single-line">
<div class="box-content-row box-content-row-flex" *ngFor="let a of cipher.attachments">
<div class="row-main">
{{ a.fileName }}
</div>
<small class="row-sub-label">{{ a.sizeName }}</small>
<div class="action-buttons no-pad">
<button
type="button"
class="row-btn btn"
type="button"
appStopClick
appA11yTitle="{{ 'deleteAttachment' | i18n }}"
(click)="delete(a)"
#deleteBtn
[appApiAction]="deletePromises[a.id]"
[disabled]="$any(deleteBtn).loading"
>
<i
class="bwi bwi-trash bwi-lg bwi-fw"
[hidden]="$any(deleteBtn).loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!$any(deleteBtn).loading"
aria-hidden="true"
></i>
</button>
</div>
</div>
</div>
</div>
<div class="box">
<h2 class="box-header">
{{ "newAttachment" | i18n }}
</h2>
<div class="box-content no-hover">
<div class="box-content-row">
<label for="file">{{ "file" | i18n }}</label>
<input type="file" id="file" name="file" aria-describedby="fileHelp" required />
</div>
</div>
<div id="fileHelp" class="box-footer">
{{ "maxFileSize" | i18n }}
</div>
</div>
</main>
</form>

View File

@@ -1,82 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Location } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { first } from "rxjs/operators";
import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
@Component({
selector: "app-vault-attachments",
templateUrl: "attachments.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class AttachmentsComponent extends BaseAttachmentsComponent implements OnInit {
openedAttachmentsInPopup: boolean;
constructor(
cipherService: CipherService,
i18nService: I18nService,
keyService: KeyService,
encryptService: EncryptService,
platformUtilsService: PlatformUtilsService,
apiService: ApiService,
private location: Location,
private route: ActivatedRoute,
stateService: StateService,
logService: LogService,
fileDownloadService: FileDownloadService,
dialogService: DialogService,
billingAccountProfileStateService: BillingAccountProfileStateService,
accountService: AccountService,
toastService: ToastService,
) {
super(
cipherService,
i18nService,
keyService,
encryptService,
platformUtilsService,
apiService,
window,
logService,
stateService,
fileDownloadService,
dialogService,
billingAccountProfileStateService,
accountService,
toastService,
);
}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {
this.cipherId = params.cipherId;
await this.init();
});
this.openedAttachmentsInPopup = history.length === 1;
}
back() {
this.location.back();
}
close() {
window.close();
}
}

View File

@@ -1,43 +0,0 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<header>
<div class="left">
<button type="button" (click)="back()">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "collections" | i18n }}</span>
</h1>
<div class="right">
<button type="submit" [disabled]="form.loading">
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
</div>
</header>
<main tabindex="-1">
<div class="box">
<div class="box-content" *ngIf="!collections || !collections.length">
<div class="box-content-row padded no-hover">
{{ "noCollectionsInList" | i18n }}
</div>
</div>
<div class="box-content" *ngIf="collections && collections.length">
<div
class="box-content-row box-content-row-checkbox"
*ngFor="let c of collections; let i = index"
appBoxRow
>
<label for="collection_{{ i }}">{{ c.name }}</label>
<input
id="collection_{{ i }}"
type="checkbox"
[(ngModel)]="$any(c).checked"
name="Collection[{{ i }}].Checked"
/>
</div>
</div>
</div>
</main>
</form>

View File

@@ -1,61 +0,0 @@
import { Location } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { first } from "rxjs/operators";
import { CollectionService } from "@bitwarden/admin-console/common";
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { ToastService } from "@bitwarden/components";
@Component({
selector: "app-vault-collections",
templateUrl: "collections.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class CollectionsComponent extends BaseCollectionsComponent implements OnInit {
constructor(
collectionService: CollectionService,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
cipherService: CipherService,
organizationService: OrganizationService,
private route: ActivatedRoute,
private location: Location,
logService: LogService,
accountService: AccountService,
toastService: ToastService,
) {
super(
collectionService,
platformUtilsService,
i18nService,
cipherService,
organizationService,
logService,
accountService,
toastService,
);
}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.onSavedCollections.subscribe(() => {
this.back();
});
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {
this.cipherId = params.cipherId;
await this.load();
});
}
back() {
this.location.back();
}
}

View File

@@ -1,95 +0,0 @@
<app-header>
<h1 class="sr-only">{{ "currentTab" | i18n }}</h1>
<div class="left">
<app-pop-out *ngIf="!inSidebar"></app-pop-out>
<button
type="button"
(click)="refresh()"
appA11yTitle="{{ 'refresh' | i18n }}"
*ngIf="inSidebar"
>
<i class="bwi bwi-refresh-tab bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
</div>
<div class="search center">
<input
type="{{ searchTypeSearch ? 'search' : 'text' }}"
placeholder="{{ 'searchVault' | i18n }}"
id="search"
[(ngModel)]="searchText"
(input)="search$.next()"
autocomplete="off"
(keydown)="closeOnEsc($event)"
appAutofocus
/>
<i class="bwi bwi-search" aria-hidden="true"></i>
</div>
<div class="right">
<button type="button" (click)="addCipher()" appA11yTitle="{{ 'addItem' | i18n }}">
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
</div>
</app-header>
<main tabindex="-1">
<div class="no-items" *ngIf="!loaded">
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
</div>
<ng-container *ngIf="loaded">
<app-vault-select (onVaultSelectionChanged)="load()"></app-vault-select>
<div class="box list" *ngIf="loginCiphers">
<h2 class="box-header">
{{ "typeLogins" | i18n }}
<span class="flex-right">{{ loginCiphers.length }}</span>
</h2>
<div class="box-content">
<app-cipher-row
*ngFor="let loginCipher of loginCiphers"
[cipher]="loginCipher"
title="{{ 'autoFill' | i18n }}"
[showView]="true"
(onSelected)="fillCipher($event)"
(onView)="viewCipher($event)"
>
</app-cipher-row>
<div class="box-content-row padded no-hover" *ngIf="!loginCiphers.length">
<p class="text-center">{{ "autoFillInfo" | i18n }}</p>
<button type="button" class="btn primary link block" (click)="addCipher()">
{{ "addLogin" | i18n }}
</button>
</div>
</div>
</div>
<div class="box list" *ngIf="cardCiphers && cardCiphers.length">
<h2 class="box-header">
{{ "cards" | i18n }}
<span class="flex-right">{{ cardCiphers.length }}</span>
</h2>
<div class="box-content">
<app-cipher-row
*ngFor="let cardCipher of cardCiphers"
[cipher]="cardCipher"
title="{{ 'autoFill' | i18n }}"
[showView]="true"
(onSelected)="fillCipher($event)"
(onView)="viewCipher($event)"
></app-cipher-row>
</div>
</div>
<div class="box list" *ngIf="identityCiphers && identityCiphers.length">
<h2 class="box-header">
{{ "identities" | i18n }}
<span class="flex-right">{{ identityCiphers.length }}</span>
</h2>
<div class="box-content">
<app-cipher-row
*ngFor="let identityCipher of identityCiphers"
[cipher]="identityCipher"
title="{{ 'autoFill' | i18n }}"
[showView]="true"
(onSelected)="fillCipher($event)"
(onView)="viewCipher($event)"
></app-cipher-row>
</div>
</div>
</ng-container>
</main>

View File

@@ -1,354 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { Subject, firstValueFrom, from, Subscription } from "rxjs";
import { debounceTime, switchMap, takeUntil } from "rxjs/operators";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SyncService } from "@bitwarden/common/platform/sync";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { PasswordRepromptService } from "@bitwarden/vault";
import { AutofillService } from "../../../../autofill/services/abstractions/autofill.service";
import { BrowserApi } from "../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
import { VaultFilterService } from "../../../services/vault-filter.service";
const BroadcasterSubscriptionId = "CurrentTabComponent";
@Component({
selector: "app-current-tab",
templateUrl: "current-tab.component.html",
})
export class CurrentTabComponent implements OnInit, OnDestroy {
pageDetails: any[] = [];
tab: chrome.tabs.Tab;
cardCiphers: CipherView[];
identityCiphers: CipherView[];
loginCiphers: CipherView[];
url: string;
hostname: string;
searchText: string;
inSidebar = false;
searchTypeSearch = false;
loaded = false;
isLoading = false;
showOrganizations = false;
showHowToAutofill = false;
autofillCalloutText: string;
protected search$ = new Subject<void>();
private destroy$ = new Subject<void>();
private collectPageDetailsSubscription: Subscription;
private totpCode: string;
private totpTimeout: number;
private loadedTimeout: number;
private searchTimeout: number;
constructor(
private platformUtilsService: PlatformUtilsService,
private cipherService: CipherService,
private autofillService: AutofillService,
private i18nService: I18nService,
private router: Router,
private ngZone: NgZone,
private broadcasterService: BroadcasterService,
private changeDetectorRef: ChangeDetectorRef,
private syncService: SyncService,
private searchService: SearchService,
private autofillSettingsService: AutofillSettingsServiceAbstraction,
private passwordRepromptService: PasswordRepromptService,
private organizationService: OrganizationService,
private vaultFilterService: VaultFilterService,
private vaultSettingsService: VaultSettingsService,
) {}
async ngOnInit() {
this.searchTypeSearch = !this.platformUtilsService.isSafari();
this.inSidebar = BrowserPopupUtils.inSidebar(window);
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.ngZone.run(async () => {
switch (message.command) {
case "syncCompleted":
if (this.isLoading) {
window.setTimeout(() => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.load();
}, 500);
}
break;
default:
break;
}
this.changeDetectorRef.detectChanges();
});
});
if (!this.syncService.syncInProgress) {
await this.load();
await this.setCallout();
} else {
this.loadedTimeout = window.setTimeout(async () => {
if (!this.isLoading) {
await this.load();
await this.setCallout();
}
}, 5000);
}
this.search$
.pipe(
debounceTime(500),
switchMap(() => {
return from(this.searchVault());
}),
takeUntil(this.destroy$),
)
.subscribe();
const autofillOnPageLoadOrgPolicy = await firstValueFrom(
this.autofillSettingsService.activateAutofillOnPageLoadFromPolicy$,
);
const autofillOnPageLoadPolicyToastHasDisplayed = await firstValueFrom(
this.autofillSettingsService.autofillOnPageLoadPolicyToastHasDisplayed$,
);
// If the org "autofill on page load" policy is set, set the user setting to match it
// @TODO override user setting instead of overwriting
if (autofillOnPageLoadOrgPolicy === true) {
await this.autofillSettingsService.setAutofillOnPageLoad(true);
if (!autofillOnPageLoadPolicyToastHasDisplayed) {
this.platformUtilsService.showToast(
"info",
null,
this.i18nService.t("autofillPageLoadPolicyActivated"),
);
await this.autofillSettingsService.setAutofillOnPageLoadPolicyToastHasDisplayed(true);
}
}
// If the org policy is ever disabled after being enabled, reset the toast notification
if (!autofillOnPageLoadOrgPolicy && autofillOnPageLoadPolicyToastHasDisplayed) {
await this.autofillSettingsService.setAutofillOnPageLoadPolicyToastHasDisplayed(false);
}
}
ngOnDestroy() {
window.clearTimeout(this.loadedTimeout);
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
this.destroy$.next();
this.destroy$.complete();
}
async refresh() {
await this.load();
}
addCipher() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/add-cipher"], {
queryParams: {
name: this.hostname,
uri: this.url,
selectedVault: this.vaultFilterService.getVaultFilter().selectedOrganizationId,
},
});
}
viewCipher(cipher: CipherView) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/view-cipher"], { queryParams: { cipherId: cipher.id } });
}
async fillCipher(cipher: CipherView, closePopupDelay?: number) {
if (
cipher.reprompt !== CipherRepromptType.None &&
!(await this.passwordRepromptService.showPasswordPrompt())
) {
return;
}
this.totpCode = null;
if (this.totpTimeout != null) {
window.clearTimeout(this.totpTimeout);
}
if (this.pageDetails == null || this.pageDetails.length === 0) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError"));
return;
}
try {
this.totpCode = await this.autofillService.doAutoFill({
tab: this.tab,
cipher: cipher,
pageDetails: this.pageDetails,
doc: window.document,
fillNewPassword: true,
allowTotpAutofill: true,
});
if (this.totpCode != null) {
this.platformUtilsService.copyToClipboard(this.totpCode, { window: window });
}
if (BrowserPopupUtils.inPopup(window)) {
if (!closePopupDelay) {
if (this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari()) {
BrowserApi.closePopup(window);
} else {
// Slight delay to fix bug in Chromium browsers where popup closes without copying totp to clipboard
setTimeout(() => BrowserApi.closePopup(window), 50);
}
} else {
setTimeout(() => BrowserApi.closePopup(window), closePopupDelay);
}
}
} catch {
this.ngZone.run(() => {
this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError"));
this.changeDetectorRef.detectChanges();
});
}
}
async searchVault() {
if (!(await this.searchService.isSearchable(this.searchText))) {
return;
}
await this.router.navigate(["/tabs/vault"], { queryParams: { searchText: this.searchText } });
}
closeOnEsc(e: KeyboardEvent) {
// If input not empty, use browser default behavior of clearing input instead
if (e.key === "Escape" && (this.searchText == null || this.searchText === "")) {
BrowserApi.closePopup(window);
}
}
protected async load() {
this.isLoading = false;
this.tab = await BrowserApi.getTabFromCurrentWindow();
if (this.tab != null) {
this.url = this.tab.url;
} else {
this.loginCiphers = [];
this.isLoading = this.loaded = true;
return;
}
this.pageDetails = [];
this.collectPageDetailsSubscription?.unsubscribe();
this.collectPageDetailsSubscription = this.autofillService
.collectPageDetailsFromTab$(this.tab)
.pipe(takeUntil(this.destroy$))
.subscribe((pageDetails) => (this.pageDetails = pageDetails));
this.hostname = Utils.getHostname(this.url);
const otherTypes: CipherType[] = [];
const dontShowCards = !(await firstValueFrom(this.vaultSettingsService.showCardsCurrentTab$));
const dontShowIdentities = !(await firstValueFrom(
this.vaultSettingsService.showIdentitiesCurrentTab$,
));
this.showOrganizations = await this.organizationService.hasOrganizations();
if (!dontShowCards) {
otherTypes.push(CipherType.Card);
}
if (!dontShowIdentities) {
otherTypes.push(CipherType.Identity);
}
const ciphers = await this.cipherService.getAllDecryptedForUrl(
this.url,
otherTypes.length > 0 ? otherTypes : null,
);
this.loginCiphers = [];
this.cardCiphers = [];
this.identityCiphers = [];
ciphers.forEach((c) => {
if (!this.vaultFilterService.filterCipherForSelectedVault(c)) {
switch (c.type) {
case CipherType.Login:
this.loginCiphers.push(c);
break;
case CipherType.Card:
this.cardCiphers.push(c);
break;
case CipherType.Identity:
this.identityCiphers.push(c);
break;
default:
break;
}
}
});
if (this.loginCiphers.length) {
this.loginCiphers = this.loginCiphers.sort((a, b) =>
this.cipherService.sortCiphersByLastUsedThenName(a, b),
);
}
this.isLoading = this.loaded = true;
}
async goToSettings() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["autofill"]);
}
async dismissCallout() {
await this.autofillSettingsService.setAutofillOnPageLoadCalloutIsDismissed(true);
this.showHowToAutofill = false;
}
private async setCallout() {
const inlineMenuVisibilityIsOff =
(await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$)) ===
AutofillOverlayVisibility.Off;
this.showHowToAutofill =
this.loginCiphers.length > 0 &&
inlineMenuVisibilityIsOff &&
!(await firstValueFrom(this.autofillSettingsService.autofillOnPageLoad$)) &&
!(await firstValueFrom(this.autofillSettingsService.autofillOnPageLoadCalloutIsDismissed$));
if (this.showHowToAutofill) {
const autofillCommand = await this.platformUtilsService.getAutofillKeyboardShortcut();
await this.setAutofillCalloutText(autofillCommand);
}
}
private setAutofillCalloutText(command: string) {
if (command) {
this.autofillCalloutText = this.i18nService.t("autofillSelectInfoWithCommand", command);
} else {
this.autofillCalloutText = this.i18nService.t("autofillSelectInfoWithoutCommand");
}
}
}

View File

@@ -1,40 +0,0 @@
<header>
<div class="left">
<button type="button" (click)="close()">{{ "close" | i18n }}</button>
</div>
<h1 class="center">
<span class="title">{{ "passwordHistory" | i18n }}</span>
</h1>
<div class="right"></div>
</header>
<main tabindex="-1">
<div class="box list full-list" *ngIf="history && history.length">
<div class="box-content">
<div class="box-content-row box-content-row-flex" *ngFor="let h of history">
<div class="row-main">
<div class="row-main-content">
<span
class="text monospaced no-ellipsis"
[innerHTML]="h.password | colorPassword"
></span>
<span class="detail">{{ h.lastUsedDate | date: "medium" }}</span>
</div>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'copyPassword' | i18n }}"
(click)="copy(h.password)"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</div>
<div class="no-items" *ngIf="!history || !history.length">
<p>{{ "noPasswordsInList" | i18n }}</p>
</div>
</main>

View File

@@ -1,44 +0,0 @@
import { Location } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { first } from "rxjs/operators";
import { PasswordHistoryComponent as BasePasswordHistoryComponent } from "@bitwarden/angular/vault/components/password-history.component";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@Component({
selector: "app-password-history",
templateUrl: "password-history.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class PasswordHistoryComponent extends BasePasswordHistoryComponent implements OnInit {
constructor(
cipherService: CipherService,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
accountService: AccountService,
private location: Location,
private route: ActivatedRoute,
) {
super(cipherService, platformUtilsService, i18nService, accountService, window);
}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {
if (params.cipherId) {
this.cipherId = params.cipherId;
} else {
this.close();
}
await this.init();
});
}
close() {
this.location.back();
}
}

View File

@@ -1,77 +0,0 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<ng-container *ngIf="organizations$ | async as organizations">
<header>
<div class="left">
<button type="button" (click)="cancel()">{{ "cancel" | i18n }}</button>
</div>
<h1 class="center">
<span class="title">{{ "moveToOrganization" | i18n }}</span>
</h1>
<div class="right">
<button
type="submit"
[disabled]="form.loading || !canSave"
*ngIf="organizations && organizations.length"
>
<span [hidden]="form.loading">{{ "move" | i18n }}</span>
<i
class="bwi bwi-spinner bwi-lg bwi-spin"
[hidden]="!form.loading"
aria-hidden="true"
></i>
</button>
</div>
</header>
<main tabindex="-1">
<div class="box">
<div class="box-content" *ngIf="!organizations || !organizations.length">
<div class="box-content-row padded no-hover">
{{ "noOrganizationsList" | i18n }}
</div>
</div>
<div class="box-content" *ngIf="organizations && organizations.length">
<div class="box-content-row" appBoxRow>
<label for="organization">{{ "organization" | i18n }}</label>
<select
id="organization"
name="OrganizationId"
aria-describedby="organizationHelp"
[(ngModel)]="organizationId"
(change)="filterCollections()"
>
<option *ngFor="let o of organizations" [ngValue]="o.id">{{ o.name }}</option>
</select>
</div>
</div>
<div id="organizationHelp" class="box-footer">
{{ "moveToOrgDesc" | i18n }}
</div>
</div>
<div class="box" *ngIf="organizations && organizations.length">
<h2 class="box-header">
{{ "collections" | i18n }}
</h2>
<div class="box-content" *ngIf="!collections || !collections.length">
<div class="box-content-row padded no-hover">
{{ "noCollectionsInList" | i18n }}
</div>
</div>
<div class="box-content" *ngIf="collections && collections.length">
<div
class="box-content-row box-content-row-checkbox"
*ngFor="let c of collections; let i = index"
appBoxRow
>
<label for="collection_{{ i }}">{{ c.name }}</label>
<input
id="collection_{{ i }}"
type="checkbox"
[(ngModel)]="c.checked"
name="Collection[{{ i }}].Checked"
/>
</div>
</div>
</div>
</main>
</ng-container>
</form>

View File

@@ -1,72 +0,0 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { CollectionService } from "@bitwarden/admin-console/common";
import { ShareComponent as BaseShareComponent } from "@bitwarden/angular/components/share.component";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@Component({
selector: "app-vault-share",
templateUrl: "share.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class ShareComponent extends BaseShareComponent implements OnInit {
constructor(
collectionService: CollectionService,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
logService: LogService,
cipherService: CipherService,
private route: ActivatedRoute,
private router: Router,
organizationService: OrganizationService,
accountService: AccountService,
) {
super(
collectionService,
platformUtilsService,
i18nService,
cipherService,
logService,
organizationService,
accountService,
);
}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.onSharedCipher.subscribe(() => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["view-cipher", { cipherId: this.cipherId }]);
});
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {
this.cipherId = params.cipherId;
await this.load();
});
}
async submit(): Promise<boolean> {
const success = await super.submit();
if (success) {
this.cancel();
}
return success;
}
cancel() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/view-cipher"], {
replaceUrl: true,
queryParams: { cipherId: this.cipher.id },
});
}
}

View File

@@ -1,238 +0,0 @@
<app-header>
<div class="left">
<app-pop-out></app-pop-out>
</div>
<h1 class="sr-only">{{ "myVault" | i18n }}</h1>
<div class="search center">
<input
type="{{ searchTypeSearch ? 'search' : 'text' }}"
placeholder="{{ 'searchVault' | i18n }}"
id="search"
[(ngModel)]="searchText"
(input)="search(200)"
autocomplete="off"
appAutofocus
(keydown)="closeOnEsc($event)"
/>
<i class="bwi bwi-search"></i>
</div>
<div class="right">
<button type="button" (click)="addCipher()" appA11yTitle="{{ 'addItem' | i18n }}">
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
</div>
</app-header>
<main tabindex="-1" cdk-scrollable>
<app-vault-select
(onVaultSelectionChanged)="vaultFilterChanged()"
class="select-index-top"
></app-vault-select>
<div class="no-items" *ngIf="(!ciphers || !ciphers.length) && !showSearching()">
<i class="bwi bwi-spinner bwi-spin bwi-3x" *ngIf="!loaded"></i>
<ng-container *ngIf="loaded">
<img class="no-items-image" aria-hidden="true" />
<p>{{ "noItemsInList" | i18n }}</p>
<button type="button" (click)="addCipher()" class="btn block primary link">
{{ "addItem" | i18n }}
</button>
</ng-container>
</div>
<ng-container *ngIf="ciphers && ciphers.length && !showSearching()">
<div class="box list" *ngIf="favoriteCiphers">
<h2 class="box-header">
{{ "favorites" | i18n }}
<span class="flex-right">{{ favoriteCiphers.length }}</span>
</h2>
<div class="box-content">
<app-cipher-row
*ngFor="let favoriteCipher of favoriteCiphers"
[cipher]="favoriteCipher"
title="{{ 'viewItem' | i18n }}"
(onSelected)="selectCipher($event)"
(launchEvent)="launchCipher($event)"
>
</app-cipher-row>
</div>
</div>
<div class="box list">
<h2 class="box-header">
{{ "types" | i18n }}
<span class="flex-right">4</span>
</h2>
<div class="box-content single-line">
<button
type="button"
class="box-content-row"
appStopClick
(click)="selectType(cipherType.Login)"
>
<div class="row-main">
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-globe"></i></div>
<span class="text">{{ "typeLogin" | i18n }}</span>
</div>
<span class="row-sub-label">
{{ typeCounts.get(cipherType.Login) || 0 }}
</span>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
</button>
<button
type="button"
class="box-content-row"
appStopClick
(click)="selectType(cipherType.Card)"
>
<div class="row-main">
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-credit-card"></i></div>
<span class="text">{{ "typeCard" | i18n }}</span>
</div>
<span class="row-sub-label">{{ typeCounts.get(cipherType.Card) || 0 }}</span>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
</button>
<button
type="button"
class="box-content-row"
appStopClick
(click)="selectType(cipherType.Identity)"
>
<div class="row-main">
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-id-card"></i></div>
<span class="text">{{ "typeIdentity" | i18n }}</span>
</div>
<span class="row-sub-label">{{ typeCounts.get(cipherType.Identity) || 0 }}</span>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
</button>
<button
type="button"
class="box-content-row"
appStopClick
(click)="selectType(cipherType.SecureNote)"
>
<div class="row-main">
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-sticky-note"></i></div>
<span class="text">{{ "typeSecureNote" | i18n }}</span>
</div>
<span class="row-sub-label">{{ typeCounts.get(cipherType.SecureNote) || 0 }}</span>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
</button>
<button
type="button"
class="box-content-row"
appStopClick
*ngIf="isSshKeysEnabled"
(click)="selectType(cipherType.SshKey)"
>
<div class="row-main">
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-key"></i></div>
<span class="text">{{ "typeSshKey" | i18n }}</span>
</div>
<span class="row-sub-label">{{ typeCounts.get(cipherType.SshKey) || 0 }}</span>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
</button>
</div>
</div>
<div class="box list" *ngIf="nestedFolders?.length">
<h2 class="box-header">
{{ "folders" | i18n }}
<span class="flex-right">{{ folderCount }}</span>
</h2>
<div class="box-content single-line">
<button
type="button"
*ngFor="let f of nestedFolders"
class="box-content-row"
appStopClick
(click)="selectFolder(f.node)"
>
<div class="row-main">
<div class="icon">
<i class="bwi bwi-fw bwi-lg bwi-folder"></i>
</div>
<span class="text">{{ f.node.name }}</span>
</div>
<span class="row-sub-label">{{ folderCounts.get(f.node.id) || 0 }}</span>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
</button>
</div>
</div>
<div class="box list" *ngIf="showCollections && nestedCollections && nestedCollections.length">
<h2 class="box-header">
{{ "collections" | i18n }}
<span class="flex-right">{{ nestedCollections.length }}</span>
</h2>
<div class="box-content single-line">
<button
type="button"
*ngFor="let nestedCollection of nestedCollections"
class="box-content-row"
appStopClick
(click)="selectCollection(nestedCollection.node)"
>
<div class="row-main">
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-collection"></i></div>
<span class="text">{{ nestedCollection.node.name }}</span>
</div>
<span class="row-sub-label">{{
collectionCounts.get(nestedCollection.node.id) || 0
}}</span>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
</button>
</div>
</div>
<div class="box list" *ngIf="showNoFolderCiphers">
<h2 class="box-header">
{{ "noneFolder" | i18n }}
<div class="flex-right">{{ noFolderCiphers.length }}</div>
</h2>
<div class="box-content">
<app-cipher-row
*ngFor="let noFolderCipher of noFolderCiphers"
[cipher]="noFolderCipher"
title="{{ 'viewItem' | i18n }}"
(onSelected)="selectCipher($event)"
(launchEvent)="launchCipher($event)"
>
</app-cipher-row>
</div>
</div>
<div class="box list" *ngIf="deletedCount">
<h2 class="box-header">
{{ "trash" | i18n }}
<span class="flex-right">{{ deletedCount }}</span>
</h2>
<div class="box-content single-line">
<button type="button" class="box-content-row" appStopClick (click)="selectTrash()">
<div class="row-main">
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-trash"></i></div>
<span class="text">{{ "trash" | i18n }}</span>
</div>
<span class="row-sub-label">{{ deletedCount }}</span>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
</button>
</div>
</div>
</ng-container>
<ng-container *ngIf="showSearching()">
<div class="no-items" *ngIf="!ciphers || !ciphers.length">
<p>{{ "noItemsInList" | i18n }}</p>
</div>
<cdk-virtual-scroll-viewport
itemSize="55"
minBufferPx="400"
maxBufferPx="600"
*ngIf="ciphers && ciphers.length > 0"
>
<div class="box list full-list">
<div class="box-content">
<app-cipher-row
*cdkVirtualFor="let searchedCipher of ciphers"
[cipher]="searchedCipher"
title="{{ 'viewItem' | i18n }}"
(onSelected)="selectCipher($event)"
(launchEvent)="launchCipher($event)"
>
</app-cipher-row>
</div>
</div>
</cdk-virtual-scroll-viewport>
</ng-container>
</main>

View File

@@ -1,482 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Location } from "@angular/common";
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs";
import { first, switchMap, takeUntil } from "rxjs/operators";
import { CollectionView } from "@bitwarden/admin-console/common";
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { BrowserGroupingsComponentState } from "../../../../models/browserGroupingsComponentState";
import { BrowserApi } from "../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
import { VaultBrowserStateService } from "../../../services/vault-browser-state.service";
import { VaultFilterService } from "../../../services/vault-filter.service";
const ComponentId = "VaultComponent";
@Component({
selector: "app-vault-filter",
templateUrl: "vault-filter.component.html",
})
export class VaultFilterComponent implements OnInit, OnDestroy {
get showNoFolderCiphers(): boolean {
return (
this.noFolderCiphers != null &&
this.noFolderCiphers.length < this.noFolderListSize &&
this.collections.length === 0
);
}
get folderCount(): number {
return this.nestedFolders.length - (this.showNoFolderCiphers ? 0 : 1);
}
folders: FolderView[];
nestedFolders: TreeNode<FolderView>[];
collections: CollectionView[];
nestedCollections: TreeNode<CollectionView>[];
loaded = false;
cipherType = CipherType;
ciphers: CipherView[];
favoriteCiphers: CipherView[];
noFolderCiphers: CipherView[];
folderCounts = new Map<string, number>();
collectionCounts = new Map<string, number>();
typeCounts = new Map<CipherType, number>();
state: BrowserGroupingsComponentState;
showLeftHeader = true;
searchPending = false;
searchTypeSearch = false;
deletedCount = 0;
vaultFilter: VaultFilter;
selectedOrganization: string = null;
showCollections = true;
isSshKeysEnabled = false;
private loadedTimeout: number;
private selectedTimeout: number;
private preventSelected = false;
private noFolderListSize = 100;
private searchTimeout: any = null;
private hasSearched = false;
private hasLoadedAllCiphers = false;
private allCiphers: CipherView[] = null;
private destroy$ = new Subject<void>();
private _searchText$ = new BehaviorSubject<string>("");
private isSearchable: boolean = false;
get searchText() {
return this._searchText$.value;
}
set searchText(value: string) {
this._searchText$.next(value);
}
constructor(
private i18nService: I18nService,
private cipherService: CipherService,
private router: Router,
private ngZone: NgZone,
private broadcasterService: BroadcasterService,
private changeDetectorRef: ChangeDetectorRef,
private route: ActivatedRoute,
private syncService: SyncService,
private platformUtilsService: PlatformUtilsService,
private searchService: SearchService,
private location: Location,
private vaultFilterService: VaultFilterService,
private vaultBrowserStateService: VaultBrowserStateService,
private configService: ConfigService,
) {
this.noFolderListSize = 100;
}
async ngOnInit() {
this.searchTypeSearch = !this.platformUtilsService.isSafari();
this.showLeftHeader = !(
BrowserPopupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()
);
await this.vaultBrowserStateService.setBrowserVaultItemsComponentState(null);
this.broadcasterService.subscribe(ComponentId, (message: any) => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.ngZone.run(async () => {
switch (message.command) {
case "syncCompleted":
window.setTimeout(() => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.load();
}, 500);
break;
default:
break;
}
this.changeDetectorRef.detectChanges();
});
});
const restoredScopeState = await this.restoreState();
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {
this.state = await this.vaultBrowserStateService.getBrowserGroupingsComponentState();
if (this.state?.searchText) {
this.searchText = this.state.searchText;
} else if (params.searchText) {
this.searchText = params.searchText;
this.location.replaceState("vault");
}
if (!this.syncService.syncInProgress) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.load();
} else {
this.loadedTimeout = window.setTimeout(() => {
if (!this.loaded) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.load();
}
}, 5000);
}
if (!this.syncService.syncInProgress || restoredScopeState) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserPopupUtils.setContentScrollY(window, this.state?.scrollY);
}
});
this._searchText$
.pipe(
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
takeUntil(this.destroy$),
)
.subscribe((isSearchable) => {
this.isSearchable = isSearchable;
});
this.isSshKeysEnabled = await this.configService.getFeatureFlag(FeatureFlag.SSHKeyVaultItem);
}
ngOnDestroy() {
if (this.loadedTimeout != null) {
window.clearTimeout(this.loadedTimeout);
}
if (this.selectedTimeout != null) {
window.clearTimeout(this.selectedTimeout);
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.saveState();
this.broadcasterService.unsubscribe(ComponentId);
this.destroy$.next();
this.destroy$.complete();
}
async load() {
this.vaultFilter = this.vaultFilterService.getVaultFilter();
this.updateSelectedOrg();
await this.loadCollectionsAndFolders();
await this.loadCiphers();
if (this.showNoFolderCiphers && this.nestedFolders.length > 0) {
// Remove "No Folder" from folder listing
this.nestedFolders = this.nestedFolders.slice(0, this.nestedFolders.length - 1);
}
this.loaded = true;
}
async loadCiphers() {
this.allCiphers = await this.cipherService.getAllDecrypted();
if (!this.hasLoadedAllCiphers) {
this.hasLoadedAllCiphers = !(await this.searchService.isSearchable(this.searchText));
}
await this.search(null);
this.getCounts();
}
async loadCollections() {
const allCollections = await this.vaultFilterService.buildCollections(
this.selectedOrganization,
);
this.collections = allCollections.fullList;
this.nestedCollections = allCollections.nestedList;
}
async loadFolders() {
const allFolders = await firstValueFrom(
this.vaultFilterService.buildNestedFolders(this.selectedOrganization),
);
this.folders = allFolders.fullList;
this.nestedFolders = allFolders.nestedList;
}
async search(timeout: number = null) {
this.searchPending = false;
if (this.searchTimeout != null) {
clearTimeout(this.searchTimeout);
}
const filterDeleted = (c: CipherView) => !c.isDeleted;
if (timeout == null) {
this.hasSearched = this.isSearchable;
this.ciphers = await this.searchService.searchCiphers(
this.searchText,
filterDeleted,
this.allCiphers,
);
this.ciphers = this.ciphers.filter(
(c) => !this.vaultFilterService.filterCipherForSelectedVault(c),
);
return;
}
this.searchPending = true;
this.searchTimeout = setTimeout(async () => {
this.hasSearched = this.isSearchable;
if (!this.hasLoadedAllCiphers && !this.hasSearched) {
await this.loadCiphers();
} else {
this.ciphers = await this.searchService.searchCiphers(
this.searchText,
filterDeleted,
this.allCiphers,
);
}
this.ciphers = this.ciphers.filter(
(c) => !this.vaultFilterService.filterCipherForSelectedVault(c),
);
this.searchPending = false;
}, timeout);
}
async selectType(type: CipherType) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/ciphers"], { queryParams: { type: type } });
}
async selectFolder(folder: FolderView) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/ciphers"], { queryParams: { folderId: folder.id || "none" } });
}
async selectCollection(collection: CollectionView) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/ciphers"], { queryParams: { collectionId: collection.id } });
}
async selectTrash() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/ciphers"], { queryParams: { deleted: true } });
}
async selectCipher(cipher: CipherView) {
this.selectedTimeout = window.setTimeout(() => {
if (!this.preventSelected) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/view-cipher"], { queryParams: { cipherId: cipher.id } });
}
this.preventSelected = false;
}, 200);
}
async launchCipher(cipher: CipherView) {
if (cipher.type !== CipherType.Login || !cipher.login.canLaunch) {
return;
}
if (this.selectedTimeout != null) {
window.clearTimeout(this.selectedTimeout);
}
this.preventSelected = true;
await this.cipherService.updateLastLaunchedDate(cipher.id);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserApi.createNewTab(cipher.login.launchUri);
if (BrowserPopupUtils.inPopup(window)) {
BrowserApi.closePopup(window);
}
}
async addCipher() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/add-cipher"], {
queryParams: { selectedVault: this.vaultFilter.selectedOrganizationId },
});
}
async vaultFilterChanged() {
if (this.showSearching) {
await this.search();
}
this.updateSelectedOrg();
await this.loadCollectionsAndFolders();
this.getCounts();
}
updateSelectedOrg() {
this.vaultFilter = this.vaultFilterService.getVaultFilter();
if (this.vaultFilter.selectedOrganizationId != null) {
this.selectedOrganization = this.vaultFilter.selectedOrganizationId;
} else {
this.selectedOrganization = null;
}
}
getCounts() {
let favoriteCiphers: CipherView[] = null;
let noFolderCiphers: CipherView[] = null;
const folderCounts = new Map<string, number>();
const collectionCounts = new Map<string, number>();
const typeCounts = new Map<CipherType, number>();
this.deletedCount = this.allCiphers.filter(
(c) => c.isDeleted && !this.vaultFilterService.filterCipherForSelectedVault(c),
).length;
this.ciphers?.forEach((c) => {
if (!this.vaultFilterService.filterCipherForSelectedVault(c)) {
if (c.isDeleted) {
return;
}
if (c.favorite) {
if (favoriteCiphers == null) {
favoriteCiphers = [];
}
favoriteCiphers.push(c);
}
if (c.folderId == null) {
if (noFolderCiphers == null) {
noFolderCiphers = [];
}
noFolderCiphers.push(c);
}
if (typeCounts.has(c.type)) {
typeCounts.set(c.type, typeCounts.get(c.type) + 1);
} else {
typeCounts.set(c.type, 1);
}
if (folderCounts.has(c.folderId)) {
folderCounts.set(c.folderId, folderCounts.get(c.folderId) + 1);
} else {
folderCounts.set(c.folderId, 1);
}
if (c.collectionIds != null) {
c.collectionIds.forEach((colId) => {
if (collectionCounts.has(colId)) {
collectionCounts.set(colId, collectionCounts.get(colId) + 1);
} else {
collectionCounts.set(colId, 1);
}
});
}
}
});
this.favoriteCiphers = favoriteCiphers;
this.noFolderCiphers = noFolderCiphers;
this.typeCounts = typeCounts;
this.folderCounts = folderCounts;
this.collectionCounts = collectionCounts;
}
showSearching() {
return this.hasSearched || (!this.searchPending && this.isSearchable);
}
closeOnEsc(e: KeyboardEvent) {
// If input not empty, use browser default behavior of clearing input instead
if (e.key === "Escape" && (this.searchText == null || this.searchText === "")) {
BrowserApi.closePopup(window);
}
}
private async loadCollectionsAndFolders() {
this.showCollections = !this.vaultFilter.myVaultOnly;
await this.loadFolders();
await this.loadCollections();
}
private async saveState() {
this.state = Object.assign(new BrowserGroupingsComponentState(), {
scrollY: BrowserPopupUtils.getContentScrollY(window),
searchText: this.searchText,
favoriteCiphers: this.favoriteCiphers,
noFolderCiphers: this.noFolderCiphers,
ciphers: this.ciphers,
collectionCounts: this.collectionCounts,
folderCounts: this.folderCounts,
typeCounts: this.typeCounts,
folders: this.folders,
collections: this.collections,
deletedCount: this.deletedCount,
});
await this.vaultBrowserStateService.setBrowserGroupingsComponentState(this.state);
}
private async restoreState(): Promise<boolean> {
this.state = await this.vaultBrowserStateService.getBrowserGroupingsComponentState();
if (this.state == null) {
return false;
}
if (this.state.favoriteCiphers != null) {
this.favoriteCiphers = this.state.favoriteCiphers;
}
if (this.state.noFolderCiphers != null) {
this.noFolderCiphers = this.state.noFolderCiphers;
}
if (this.state.ciphers != null) {
this.ciphers = this.state.ciphers;
}
if (this.state.collectionCounts != null) {
this.collectionCounts = this.state.collectionCounts;
}
if (this.state.folderCounts != null) {
this.folderCounts = this.state.folderCounts;
}
if (this.state.typeCounts != null) {
this.typeCounts = this.state.typeCounts;
}
if (this.state.folders != null) {
this.folders = this.state.folders;
}
if (this.state.collections != null) {
this.collections = this.state.collections;
}
if (this.state.deletedCount != null) {
this.deletedCount = this.state.deletedCount;
}
return true;
}
}

View File

@@ -1,123 +0,0 @@
<header>
<div class="left">
<button type="button" (click)="back()">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="sr-only">{{ "myVault" | i18n }}</h1>
<div class="search">
<input
type="{{ searchTypeSearch ? 'search' : 'text' }}"
placeholder="{{ searchPlaceholder || ('searchVault' | i18n) }}"
id="search"
[(ngModel)]="searchText"
(input)="search(200)"
autocomplete="off"
appAutofocus
/>
<i class="bwi bwi-search" aria-hidden="true"></i>
</div>
<div class="right">
<button type="button" (click)="addCipher()" appA11yTitle="{{ 'addItem' | i18n }}">
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
</div>
</header>
<main tabindex="-1" [ngClass]="{ 'stacked-boxes': showGroupings() }">
<ng-container *ngIf="showGroupings()">
<app-vault-select
*ngIf="showVaultFilter"
(onVaultSelectionChanged)="changeVaultSelection()"
></app-vault-select>
<div class="box list" *ngIf="nestedFolders && nestedFolders.length">
<h2 class="box-header">
{{ "folders" | i18n }}
</h2>
<div class="box-content single-line">
<button
type="button"
*ngFor="let f of nestedFolders"
class="box-content-row"
appStopClick
(click)="selectFolder(f.node)"
>
<div class="row-main">
<div class="icon">
<i class="bwi bwi-fw bwi-lg bwi-folder" aria-hidden="true"></i>
</div>
<span class="text">{{ f.node.name }}</span>
</div>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i></span>
</button>
</div>
</div>
<div class="box list" *ngIf="nestedCollections && nestedCollections.length">
<h2 class="box-header">
{{ "collections" | i18n }}
</h2>
<div class="box-content single-line">
<button
type="button"
*ngFor="let c of nestedCollections"
class="box-content-row"
appStopClick
(click)="selectCollection(c.node)"
>
<div class="row-main">
<div class="icon">
<i class="bwi bwi-fw bwi-lg bwi-collection" aria-hidden="true"></i>
</div>
<span class="text">{{ c.node.name }}</span>
</div>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i></span>
</button>
</div>
</div>
</ng-container>
<ng-container *ngIf="ciphers">
<div *ngIf="!ciphers.length">
<app-vault-select
*ngIf="showVaultFilter && !showGroupings()"
(onVaultSelectionChanged)="changeVaultSelection()"
></app-vault-select>
<div class="no-items" *ngIf="!nestedFolders?.length && !nestedCollections?.length">
<i class="bwi bwi-spinner bwi-spin bwi-3x" *ngIf="!loaded" aria-hidden="true"></i>
<ng-container *ngIf="loaded">
<img class="no-items-image" aria-hidden="true" />
<p>{{ "noItemsInList" | i18n }}</p>
<button type="button" (click)="addCipher()" class="btn block primary link">
{{ "addItem" | i18n }}
</button>
</ng-container>
</div>
</div>
<cdk-virtual-scroll-viewport
itemSize="55"
minBufferPx="400"
maxBufferPx="600"
*ngIf="ciphers.length"
#virtualScrollViewport
><app-vault-select
*ngIf="showVaultFilter && !showGroupings()"
(onVaultSelectionChanged)="changeVaultSelection()"
></app-vault-select>
<div class="box list only-list">
<h2 class="box-header">
{{ groupingTitle }}
<span class="flex-right">{{ isSearching() ? ciphers.length : ciphers.length }}</span>
</h2>
<div class="box-content">
<app-cipher-row
*cdkVirtualFor="let c of ciphers; let last = last"
[cipher]="c"
[last]="last"
title="{{ 'viewItem' | i18n }}"
(onSelected)="selectCipher($event)"
(launchEvent)="launchCipher($event)"
></app-cipher-row>
</div>
</div>
</cdk-virtual-scroll-viewport>
</ng-container>
</main>

View File

@@ -1,316 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Location } from "@angular/common";
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angular/vault/components/vault-items.component";
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { BrowserComponentState } from "../../../../models/browserComponentState";
import { BrowserApi } from "../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
import { VaultBrowserStateService } from "../../../services/vault-browser-state.service";
import { VaultFilterService } from "../../../services/vault-filter.service";
const ComponentId = "VaultItemsComponent";
@Component({
selector: "app-vault-items",
templateUrl: "vault-items.component.html",
})
export class VaultItemsComponent extends BaseVaultItemsComponent implements OnInit, OnDestroy {
groupingTitle: string;
state: BrowserComponentState;
folderId: string = null;
collectionId: string = null;
type: CipherType = null;
nestedFolders: TreeNode<FolderView>[];
nestedCollections: TreeNode<CollectionView>[];
searchTypeSearch = false;
showOrganizations = false;
vaultFilter: VaultFilter;
deleted = true;
noneFolder = false;
showVaultFilter = false;
private selectedTimeout: number;
private preventSelected = false;
private applySavedState = true;
private scrollingContainer = "cdk-virtual-scroll-viewport";
constructor(
searchService: SearchService,
private organizationService: OrganizationService,
private route: ActivatedRoute,
private router: Router,
private location: Location,
private ngZone: NgZone,
private broadcasterService: BroadcasterService,
private changeDetectorRef: ChangeDetectorRef,
private stateService: VaultBrowserStateService,
private i18nService: I18nService,
private collectionService: CollectionService,
private platformUtilsService: PlatformUtilsService,
cipherService: CipherService,
private vaultFilterService: VaultFilterService,
) {
super(searchService, cipherService);
this.applySavedState =
(window as any).previousPopupUrl != null &&
!(window as any).previousPopupUrl.startsWith("/ciphers");
}
async ngOnInit() {
this.searchTypeSearch = !this.platformUtilsService.isSafari();
this.showOrganizations = await this.organizationService.hasOrganizations();
this.vaultFilter = this.vaultFilterService.getVaultFilter();
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {
if (this.applySavedState) {
this.state = await this.stateService.getBrowserVaultItemsComponentState();
if (this.state?.searchText) {
this.searchText = this.state.searchText;
}
}
if (params.deleted) {
this.showVaultFilter = true;
this.groupingTitle = this.i18nService.t("trash");
this.searchPlaceholder = this.i18nService.t("searchTrash");
await this.load(this.buildFilter(), true);
} else if (params.type) {
this.showVaultFilter = true;
this.searchPlaceholder = this.i18nService.t("searchType");
this.type = parseInt(params.type, null);
switch (this.type) {
case CipherType.Login:
this.groupingTitle = this.i18nService.t("logins");
break;
case CipherType.Card:
this.groupingTitle = this.i18nService.t("cards");
break;
case CipherType.Identity:
this.groupingTitle = this.i18nService.t("identities");
break;
case CipherType.SecureNote:
this.groupingTitle = this.i18nService.t("secureNotes");
break;
case CipherType.SshKey:
this.groupingTitle = this.i18nService.t("sshKeys");
break;
default:
break;
}
await this.load(this.buildFilter());
} else if (params.folderId) {
this.showVaultFilter = true;
this.folderId = params.folderId === "none" ? null : params.folderId;
this.searchPlaceholder = this.i18nService.t("searchFolder");
if (this.folderId != null) {
this.showOrganizations = false;
const folderNode = await this.vaultFilterService.getFolderNested(this.folderId);
if (folderNode != null && folderNode.node != null) {
this.groupingTitle = folderNode.node.name;
this.nestedFolders =
folderNode.children != null && folderNode.children.length > 0
? folderNode.children
: null;
}
} else {
this.noneFolder = true;
this.groupingTitle = this.i18nService.t("noneFolder");
}
await this.load(this.buildFilter());
} else if (params.collectionId) {
this.showVaultFilter = false;
this.collectionId = params.collectionId;
this.searchPlaceholder = this.i18nService.t("searchCollection");
const collectionNode = await this.collectionService.getNested(this.collectionId);
if (collectionNode != null && collectionNode.node != null) {
this.groupingTitle = collectionNode.node.name;
this.nestedCollections =
collectionNode.children != null && collectionNode.children.length > 0
? collectionNode.children
: null;
}
await this.load(
(c) => c.collectionIds != null && c.collectionIds.indexOf(this.collectionId) > -1,
);
} else {
this.showVaultFilter = true;
this.groupingTitle = this.i18nService.t("allItems");
await this.load(this.buildFilter());
}
if (this.applySavedState && this.state != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserPopupUtils.setContentScrollY(window, this.state.scrollY, {
delay: 0,
containerSelector: this.scrollingContainer,
});
}
await this.stateService.setBrowserVaultItemsComponentState(null);
});
this.broadcasterService.subscribe(ComponentId, (message: any) => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.ngZone.run(async () => {
switch (message.command) {
case "syncCompleted":
if (message.successfully) {
window.setTimeout(() => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.refresh();
}, 500);
}
break;
default:
break;
}
this.changeDetectorRef.detectChanges();
});
});
}
ngOnDestroy() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.saveState();
this.broadcasterService.unsubscribe(ComponentId);
}
selectCipher(cipher: CipherView) {
this.selectedTimeout = window.setTimeout(() => {
if (!this.preventSelected) {
super.selectCipher(cipher);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/view-cipher"], {
queryParams: { cipherId: cipher.id, collectionId: this.collectionId },
});
}
this.preventSelected = false;
}, 200);
}
selectFolder(folder: FolderView) {
if (folder.id != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/ciphers"], { queryParams: { folderId: folder.id } });
}
}
selectCollection(collection: CollectionView) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/ciphers"], { queryParams: { collectionId: collection.id } });
}
async launchCipher(cipher: CipherView) {
if (cipher.type !== CipherType.Login || !cipher.login.canLaunch) {
return;
}
if (this.selectedTimeout != null) {
window.clearTimeout(this.selectedTimeout);
}
this.preventSelected = true;
await this.cipherService.updateLastLaunchedDate(cipher.id);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserApi.createNewTab(cipher.login.launchUri);
if (BrowserPopupUtils.inPopup(window)) {
BrowserApi.closePopup(window);
}
}
addCipher() {
if (this.deleted) {
return false;
}
super.addCipher();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/add-cipher"], {
queryParams: {
folderId: this.folderId,
type: this.type,
collectionId: this.collectionId,
selectedVault: this.vaultFilter.selectedOrganizationId,
},
});
}
back() {
(window as any).routeDirection = "b";
this.location.back();
}
showGroupings() {
return (
!this.isSearching() &&
((this.nestedFolders && this.nestedFolders.length) ||
(this.nestedCollections && this.nestedCollections.length))
);
}
async changeVaultSelection() {
this.vaultFilter = this.vaultFilterService.getVaultFilter();
await this.load(this.buildFilter(), this.deleted);
}
private buildFilter(): (cipher: CipherView) => boolean {
return (cipher) => {
let cipherPassesFilter = true;
if (this.deleted && cipherPassesFilter) {
cipherPassesFilter = cipher.isDeleted;
}
if (this.type != null && cipherPassesFilter) {
cipherPassesFilter = cipher.type === this.type;
}
if (this.folderId != null && this.folderId != "none" && cipherPassesFilter) {
cipherPassesFilter = cipher.folderId === this.folderId;
}
if (this.noneFolder) {
cipherPassesFilter = cipher.folderId == null;
}
if (this.collectionId != null && cipherPassesFilter) {
cipherPassesFilter =
cipher.collectionIds != null && cipher.collectionIds.indexOf(this.collectionId) > -1;
}
if (this.vaultFilter.selectedOrganizationId != null && cipherPassesFilter) {
cipherPassesFilter = cipher.organizationId === this.vaultFilter.selectedOrganizationId;
}
if (this.vaultFilter.myVaultOnly && cipherPassesFilter) {
cipherPassesFilter = cipher.organizationId === null;
}
return cipherPassesFilter;
};
}
private async saveState() {
this.state = {
scrollY: BrowserPopupUtils.getContentScrollY(window, this.scrollingContainer),
searchText: this.searchText,
};
await this.stateService.setBrowserVaultItemsComponentState(this.state);
}
}

View File

@@ -1,82 +0,0 @@
<ng-container *ngIf="loaded && organizations$ | async as organizations">
<div class="content org-filter-content" *ngIf="loaded && shouldShow(organizations)">
<ng-container *ngIf="selectedVault$ | async as vaultFilterDisplay">
<button
type="button"
#toggleVaults
class="org-filter"
(click)="openOverlay()"
aria-haspopup="menu"
aria-controls="cdk-overlay-container"
[attr.aria-expanded]="isOpen"
[attr.aria-label]="vaultFilterDisplay"
>
<span class="org-filter-text-container">
<span class="org-filter-text-name">{{ vaultFilterDisplay }}</span
>&nbsp;
<span
><i
class="bwi bwi-sm"
aria-hidden="true"
[ngClass]="{ 'bwi-angle-down': !isOpen, 'bwi-angle-up': isOpen }"
></i></span
></span>
</button>
</ng-container>
<ng-template class="vault-select-container" #vaultSelectorTemplate>
<div
class="vault-select"
[@transformPanel]="'open'"
cdkTrapFocus
cdkTrapFocusAutoCapture
role="dialog"
aria-modal="true"
>
<button type="button" appStopClick (click)="selectAllVaults()">
<div class="vault-select-org-text-container">
<i class="bwi bwi-fw bwi-filter vault-select-prefix-icon" aria-hidden="true"></i>
<span class="vault-select-org-name">{{ "allVaults" | i18n }}</span>
</div>
</button>
<button
type="button"
*ngIf="!enforcePersonalOwnership"
appStopClick
(click)="selectMyVault()"
>
<div class="vault-select-org-text-container">
<i class="bwi bwi-fw bwi-user vault-select-prefix-icon" aria-hidden="true"></i>
<span class="vault-select-org-name">{{ "myVault" | i18n }}</span>
</div>
</button>
<button
type="button"
*ngFor="let organization of organizations"
appStopClick
(click)="selectOrganization(organization)"
>
<div class="vault-select-org-text-container">
<i
*ngIf="organization.productTierType !== 1"
class="bwi bwi-fw bwi-business vault-select-prefix-icon"
aria-hidden="true"
></i>
<i
*ngIf="organization.productTierType === 1"
class="bwi bwi-fw bwi-family vault-select-prefix-icon"
aria-hidden="true"
></i>
<span class="vault-select-org-name">{{ organization.name }}</span
><i
*ngIf="!organization.enabled"
class="bwi bwi-fw bwi-exclamation-triangle text-danger vault-select-suffix-icon"
attr.aria-label="{{ 'organizationIsDisabled' | i18n }}"
appA11yTitle="{{ 'organizationIsDisabled' | i18n }}"
></i>
</div>
</button>
</div>
</ng-template>
</div>
</ng-container>

View File

@@ -1,227 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { animate, state, style, transition, trigger } from "@angular/animations";
import { ConnectedPosition, Overlay, OverlayRef } from "@angular/cdk/overlay";
import { TemplatePortal } from "@angular/cdk/portal";
import {
Component,
ElementRef,
EventEmitter,
HostListener,
OnDestroy,
OnInit,
Output,
TemplateRef,
ViewChild,
ViewContainerRef,
} from "@angular/core";
import {
BehaviorSubject,
combineLatest,
concatMap,
map,
merge,
Observable,
Subject,
takeUntil,
} from "rxjs";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { VaultFilterService } from "../../../services/vault-filter.service";
@Component({
selector: "app-vault-select",
templateUrl: "vault-select.component.html",
animations: [
trigger("transformPanel", [
state(
"void",
style({
opacity: 0,
}),
),
transition(
"void => open",
animate(
"100ms linear",
style({
opacity: 1,
}),
),
),
transition("* => void", animate("100ms linear", style({ opacity: 0 }))),
]),
],
})
export class VaultSelectComponent implements OnInit, OnDestroy {
@Output() onVaultSelectionChanged = new EventEmitter();
@ViewChild("toggleVaults", { read: ElementRef })
buttonRef: ElementRef<HTMLButtonElement>;
@ViewChild("vaultSelectorTemplate", { read: TemplateRef }) templateRef: TemplateRef<HTMLElement>;
private _selectedVault = new BehaviorSubject<string | null>(null);
isOpen = false;
loaded = false;
organizations$: Observable<Organization[]>;
selectedVault$: Observable<string | null> = this._selectedVault.asObservable();
enforcePersonalOwnership = false;
overlayPosition: ConnectedPosition[] = [
{
originX: "start",
originY: "bottom",
overlayX: "start",
overlayY: "top",
},
];
private overlayRef: OverlayRef;
private _destroy = new Subject<void>();
shouldShow(organizations: Organization[]): boolean {
return (
(organizations.length > 0 && !this.enforcePersonalOwnership) ||
(organizations.length > 1 && this.enforcePersonalOwnership)
);
}
constructor(
private vaultFilterService: VaultFilterService,
private i18nService: I18nService,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private platformUtilsService: PlatformUtilsService,
private organizationService: OrganizationService,
private policyService: PolicyService,
) {}
@HostListener("document:keydown.escape", ["$event"])
handleKeyboardEvent(event: KeyboardEvent) {
if (this.isOpen) {
event.preventDefault();
this.close();
}
}
async ngOnInit() {
this.organizations$ = this.organizationService.memberOrganizations$
.pipe(takeUntil(this._destroy))
.pipe(map((orgs) => orgs.sort(Utils.getSortFunction(this.i18nService, "name"))));
combineLatest([
this.organizations$,
this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership),
])
.pipe(
concatMap(async ([organizations, enforcePersonalOwnership]) => {
this.enforcePersonalOwnership = enforcePersonalOwnership;
if (this.shouldShow(organizations)) {
if (this.enforcePersonalOwnership && !this.vaultFilterService.vaultFilter.myVaultOnly) {
const firstOrganization = organizations[0];
this._selectedVault.next(firstOrganization.name);
this.vaultFilterService.setVaultFilter(firstOrganization.id);
} else if (this.vaultFilterService.vaultFilter.myVaultOnly) {
this._selectedVault.next(this.i18nService.t(this.vaultFilterService.myVault));
} else if (this.vaultFilterService.vaultFilter.selectedOrganizationId != null) {
const selectedOrganization = organizations.find(
(o) => o.id === this.vaultFilterService.vaultFilter.selectedOrganizationId,
);
this._selectedVault.next(selectedOrganization.name);
} else {
this._selectedVault.next(this.i18nService.t(this.vaultFilterService.allVaults));
}
}
}),
)
.pipe(takeUntil(this._destroy))
.subscribe();
this.loaded = true;
}
ngOnDestroy(): void {
this._destroy.next();
this._destroy.complete();
this._selectedVault.complete();
}
openOverlay() {
const viewPortHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
const positionStrategyBuilder = this.overlay.position();
const positionStrategy = positionStrategyBuilder
.flexibleConnectedTo(this.buttonRef.nativeElement)
.withFlexibleDimensions(true)
.withPush(true)
.withViewportMargin(10)
.withGrowAfterOpen(true)
.withPositions(this.overlayPosition);
this.overlayRef = this.overlay.create({
hasBackdrop: true,
positionStrategy,
maxHeight: viewPortHeight - 160,
backdropClass: "cdk-overlay-transparent-backdrop",
scrollStrategy: this.overlay.scrollStrategies.close(),
});
const templatePortal = new TemplatePortal(this.templateRef, this.viewContainerRef);
this.overlayRef.attach(templatePortal);
this.isOpen = true;
// Handle closing
merge(
this.overlayRef.outsidePointerEvents(),
this.overlayRef.backdropClick(),
this.overlayRef.detachments(),
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
).subscribe(() => {
this.close();
});
}
close() {
if (this.overlayRef) {
this.overlayRef.dispose();
this.overlayRef = undefined;
}
this.isOpen = false;
}
selectOrganization(organization: Organization) {
if (!organization.enabled) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("disabledOrganizationFilterError"),
);
} else {
this._selectedVault.next(organization.name);
this.vaultFilterService.setVaultFilter(organization.id);
this.onVaultSelectionChanged.emit();
this.close();
}
}
selectAllVaults() {
this._selectedVault.next(this.i18nService.t(this.vaultFilterService.allVaults));
this.vaultFilterService.setVaultFilter(this.vaultFilterService.allVaults);
this.onVaultSelectionChanged.emit();
this.close();
}
selectMyVault() {
this._selectedVault.next(this.i18nService.t(this.vaultFilterService.myVault));
this.vaultFilterService.setVaultFilter(this.vaultFilterService.myVault);
this.onVaultSelectionChanged.emit();
this.close();
}
}

View File

@@ -1,98 +0,0 @@
<ng-container>
<h2 class="box-header">
{{ "customFields" | i18n }}
</h2>
<div class="box-content">
<div class="box-content-row box-content-row-flex" *ngFor="let field of cipher.fields">
<div class="row-main">
<span
*ngIf="field.type != fieldType.Linked"
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, field.value)"
>{{ field.name }}</span
>
<span *ngIf="field.type === fieldType.Linked" class="row-label">{{ field.name }}</span>
<div *ngIf="field.type === fieldType.Text">
{{ field.value || "&nbsp;" }}
</div>
<div *ngIf="field.type === fieldType.Hidden">
<span *ngIf="!field.showValue" class="monospaced">{{ field.maskedValue }}</span>
<span
*ngIf="field.showValue && !field.showCount"
class="monospaced show-whitespace"
[innerHTML]="field.value | colorPassword"
></span>
<span
*ngIf="field.showValue && field.showCount"
[innerHTML]="field.value | colorPasswordCount"
></span>
</div>
<div *ngIf="field.type === fieldType.Boolean">
<i class="bwi bwi-check-square" *ngIf="field.value === 'true'" aria-hidden="true"></i>
<i class="bwi bwi-square" *ngIf="field.value !== 'true'" aria-hidden="true"></i>
<span class="sr-only">{{ field.value }}</span>
</div>
<div *ngIf="field.type === fieldType.Linked" class="box-content-row-flex">
<div class="icon icon-small">
<i
class="bwi bwi-link"
aria-hidden="true"
appA11yTitle="{{ 'linkedValue' | i18n }}"
></i>
<span class="sr-only">{{ "linkedValue" | i18n }}</span>
</div>
<span>{{ cipher.linkedFieldI18nKey(field.linkedId) | i18n }}</span>
</div>
</div>
<div class="action-buttons action-buttons-fixed">
<button
type="button"
class="row-btn"
appStopClick
attr.aria-label="{{ 'toggleCharacterCount' | i18n }} {{ field.name }}"
appA11yTitle="{{ 'toggleCharacterCount' | i18n }}"
*ngIf="field.type === fieldType.Hidden && cipher.viewPassword && field.showValue"
(click)="toggleFieldCount(field)"
[attr.aria-pressed]="field.showCount"
>
<i class="bwi bwi-lg bwi-numbered-list" aria-hidden="true"></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
attr.aria-label="{{ 'toggleVisibility' | i18n }} {{ field.name }}"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
*ngIf="field.type === fieldType.Hidden && cipher.viewPassword"
(click)="toggleFieldValue(field)"
[attr.aria-pressed]="field.showValue"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !field.showValue, 'bwi-eye-slash': field.showValue }"
></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
attr.aria-label="{{ 'copyValue' | i18n }} {{ field.name }}"
appA11yTitle="{{ 'copyValue' | i18n }}"
*ngIf="
field.value &&
field.type !== fieldType.Boolean &&
field.type !== fieldType.Linked &&
!(field.type === fieldType.Hidden && !cipher.viewPassword)
"
(click)="
copy(field.value, 'value', field.type === fieldType.Hidden ? 'H_Field' : 'Field')
"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</ng-container>

View File

@@ -1,14 +0,0 @@
import { Component } from "@angular/core";
import { ViewCustomFieldsComponent as BaseViewCustomFieldsComponent } from "@bitwarden/angular/vault/components/view-custom-fields.component";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
@Component({
selector: "app-vault-view-custom-fields",
templateUrl: "view-custom-fields.component.html",
})
export class ViewCustomFieldsComponent extends BaseViewCustomFieldsComponent {
constructor(eventCollectionService: EventCollectionService) {
super(eventCollectionService);
}
}

View File

@@ -1,719 +0,0 @@
<header>
<div class="left">
<button type="button" (click)="close()">{{ "close" | i18n }}</button>
</div>
<h1 class="center">
<span class="title">{{ "viewItem" | i18n }}</span>
</h1>
<div class="right" *ngIf="cipher">
<button type="button" (click)="edit()" *ngIf="!cipher.isDeleted">
{{ "edit" | i18n }}
</button>
</div>
</header>
<main tabindex="-1" *ngIf="cipher">
<div class="box">
<h2 class="box-header">
{{ "itemInformation" | i18n }}
</h2>
<div class="box-content">
<div class="box-content-row">
<label
for="name"
class="draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.name)"
>{{ "name" | i18n }}</label
>
<input id="name" type="text" [value]="cipher.name" readonly aria-readonly="true" />
</div>
<!-- Login -->
<div *ngIf="cipher.login">
<div class="box-content-row box-content-row-flex" *ngIf="cipher.login.username">
<div class="row-main">
<label
for="loginUsername"
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.login.username)"
>{{ "username" | i18n }}
</label>
<input
id="loginUsername"
type="text"
[value]="cipher.login.username"
readonly
aria-readonly="true"
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'copyUsername' | i18n }}"
(click)="copy(cipher.login.username, 'username', 'Username')"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="box-content-row box-content-row-flex" *ngIf="cipher.login.password">
<div class="row-main">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.login.password)"
>{{ "password" | i18n }}</span
>
<div *ngIf="!showPassword" class="monospaced">
{{ cipher.login.maskedPassword }}
</div>
<div
*ngIf="showPassword && !showPasswordCount"
class="monospaced password-wrapper"
[appCopyText]="cipher.login.password"
[innerHTML]="cipher.login.password | colorPassword"
></div>
<div
*ngIf="showPassword && showPasswordCount"
[innerHTML]="cipher.login.password | colorPasswordCount"
></div>
</div>
<div class="action-buttons action-buttons-fixed">
<button
type="button"
#checkPasswordBtn
class="row-btn btn"
appA11yTitle="{{ 'checkPassword' | i18n }}"
(click)="checkPassword()"
[appApiAction]="checkPasswordPromise"
[disabled]="$any(checkPasswordBtn).loading"
*ngIf="cipher.viewPassword"
>
<i
class="bwi bwi-lg bwi-check-circle"
[hidden]="$any(checkPasswordBtn).loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-lg bwi-spinner bwi-spin"
[hidden]="!$any(checkPasswordBtn).loading"
aria-hidden="true"
></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
attr.aria-label="{{ 'toggleCharacterCount' | i18n }} {{ 'password' | i18n }}"
appA11yTitle="{{ 'toggleCharacterCount' | i18n }}"
(click)="togglePasswordCount()"
*ngIf="showPassword"
[attr.aria-pressed]="showPasswordCount"
>
<i class="bwi bwi-lg bwi-numbered-list" aria-hidden="true"></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
attr.aria-label="{{ 'toggleVisibility' | i18n }} {{ 'password' | i18n }}"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword()"
*ngIf="cipher.viewPassword"
[attr.aria-pressed]="showPassword"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'copyPassword' | i18n }}"
(click)="copy(cipher.login.password, 'password', 'Password')"
*ngIf="cipher.viewPassword"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
<!--Passkey-->
<div
class="box"
*ngIf="cipher.login.hasFido2Credentials"
tabindex="0"
attr.aria-label="{{ 'typePasskey' | i18n }} {{ fido2CredentialCreationDateValue }}"
>
<div class="box-content">
<div class="box-content-row text-muted">
<span class="row-label">{{ "typePasskey" | i18n }}</span>
{{ fido2CredentialCreationDateValue }}
</div>
</div>
</div>
<div
class="box-content-row box-content-row-flex totp"
[ngClass]="{ low: totpLow }"
*ngIf="cipher.login.totp && totpCode"
>
<div class="row-main">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, totpCode)"
>{{ "verificationCodeTotp" | i18n }}</span
>
<span class="totp-code">{{ totpCodeFormatted }}</span>
</div>
<span class="totp-countdown" aria-hidden="true">
<span class="totp-sec">{{ totpSec }}</span>
<svg>
<g>
<circle
class="totp-circle inner"
r="12.6"
cy="16"
cx="16"
[ngStyle]="{ 'stroke-dashoffset.px': totpDash }"
></circle>
<circle class="totp-circle outer" r="14" cy="16" cx="16"></circle>
</g>
</svg>
</span>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
title="{{ 'copyVerificationCode' | i18n }}"
(click)="copy(totpCode, 'verificationCodeTotp', 'TOTP')"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
<span class="sr-only">{{ "copyValue" | i18n }}</span>
<span
class="sr-only exists-only-on-parent-focus"
aria-live="polite"
aria-atomic="true"
>{{ totpSec }}</span
>
</button>
</div>
</div>
<div class="box-content-row box-content-row-flex totp" *ngIf="showPremiumRequiredTotp">
<div class="row-main">
<span class="row-label">{{ "verificationCodeTotp" | i18n }}</span>
<span class="row-label">
<a routerLink="/premium">
{{ "premiumSubcriptionRequired" | i18n }}
</a>
</span>
</div>
</div>
</div>
<!-- Card -->
<div *ngIf="cipher.card">
<div class="box-content-row" *ngIf="cipher.card.cardholderName">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.card.cardholderName)"
>{{ "cardholderName" | i18n }}</span
>
{{ cipher.card.cardholderName }}
</div>
<div class="box-content-row box-content-row-flex" *ngIf="cipher.card.number">
<div class="row-main">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.card.number)"
>{{ "number" | i18n }}</span
>
<span [hidden]="showCardNumber" class="monospaced">{{
cipher.card.maskedNumber | creditCardNumber: cipher.card.brand
}}</span>
<span [hidden]="!showCardNumber" class="monospaced">{{
cipher.card.number | creditCardNumber: cipher.card.brand
}}</span>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
attr.aria-label="{{ 'toggleVisibility' | i18n }} {{ 'number' | i18n }}"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleCardNumber()"
[attr.aria-pressed]="showCardNumber"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showCardNumber, 'bwi-eye-slash': showCardNumber }"
></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'copyNumber' | i18n }}"
(click)="copy(cipher.card.number, 'number', 'Card Number')"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="box-content-row" *ngIf="cipher.card.brand">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.card.brand)"
>{{ "brand" | i18n }}</span
>
{{ cipher.card.brand }}
</div>
<div class="box-content-row" *ngIf="cipher.card.expiration">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.card.expiration)"
>{{ "expiration" | i18n }}</span
>
{{ cipher.card.expiration }}
</div>
<div class="box-content-row box-content-row-flex" *ngIf="cipher.card.code">
<div class="row-main">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.card.code)"
>{{ "securityCode" | i18n }}</span
>
<span [hidden]="showCardCode" class="monospaced">{{ cipher.card.maskedCode }}</span>
<span [hidden]="!showCardCode" class="monospaced">{{ cipher.card.code }}</span>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
attr.aria-label="{{ 'toggleVisibility' | i18n }} {{ 'securityCode' | i18n }}"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleCardCode()"
[attr.aria-pressed]="showCardCode"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showCardCode, 'bwi-eye-slash': showCardCode }"
></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'copySecurityCode' | i18n }}"
(click)="copy(cipher.card.code, 'securityCode', 'Security Code')"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<!-- Identity -->
<div *ngIf="cipher.identity">
<div class="box-content-row" *ngIf="cipher.identity.fullName">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.identity.fullName)"
>{{ "identityName" | i18n }}</span
>
{{ cipher.identity.fullName }}
</div>
<div class="box-content-row" *ngIf="cipher.identity.username">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.identity.username)"
>{{ "username" | i18n }}</span
>
{{ cipher.identity.username }}
</div>
<div class="box-content-row" *ngIf="cipher.identity.company">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.identity.company)"
>{{ "company" | i18n }}</span
>
{{ cipher.identity.company }}
</div>
<div class="box-content-row" *ngIf="cipher.identity.ssn">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.identity.ssn)"
>{{ "ssn" | i18n }}</span
>
{{ cipher.identity.ssn }}
</div>
<div class="box-content-row" *ngIf="cipher.identity.passportNumber">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.identity.passportNumber)"
>{{ "passportNumber" | i18n }}</span
>
{{ cipher.identity.passportNumber }}
</div>
<div class="box-content-row" *ngIf="cipher.identity.licenseNumber">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.identity.licenseNumber)"
>{{ "licenseNumber" | i18n }}</span
>
{{ cipher.identity.licenseNumber }}
</div>
<div class="box-content-row" *ngIf="cipher.identity.email">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.identity.email)"
>{{ "email" | i18n }}</span
>
{{ cipher.identity.email }}
</div>
<div class="box-content-row" *ngIf="cipher.identity.phone">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.identity.phone)"
>{{ "phone" | i18n }}</span
>
{{ cipher.identity.phone }}
</div>
<div
class="box-content-row"
*ngIf="cipher.identity.address1 || cipher.identity.city || cipher.identity.country"
>
<span
class="row-label draggable"
draggable="true"
(dragstart)="
setTextDataOnDrag(
$event,
(cipher.identity.address1 ? cipher.identity.address1 + '\n' : '') +
(cipher.identity.address2 ? cipher.identity.address2 + '\n' : '') +
(cipher.identity.address3 ? cipher.identity.address3 + '\n' : '') +
(cipher.identity.fullAddressPart2
? cipher.identity.fullAddressPart2 + '\n'
: '') +
(cipher.identity.country ? cipher.identity.country : '')
)
"
>{{ "address" | i18n }}</span
>
<div *ngIf="cipher.identity.address1">{{ cipher.identity.address1 }}</div>
<div *ngIf="cipher.identity.address2">{{ cipher.identity.address2 }}</div>
<div *ngIf="cipher.identity.address3">{{ cipher.identity.address3 }}</div>
<div *ngIf="cipher.identity.fullAddressPart2">{{ cipher.identity.fullAddressPart2 }}</div>
<div *ngIf="cipher.identity.country">{{ cipher.identity.country }}</div>
</div>
</div>
<!-- SshKey -->
<div *ngIf="cipher.sshKey">
<div class="box-content-row" *ngIf="cipher.sshKey.privateKey" style="overflow: hidden">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.sshKey.privateKey)"
>
{{ "sshPrivateKey" | i18n }}
</span>
<div [innerText]="cipher.sshKey.maskedPrivateKey" class="monospaced"></div>
</div>
<div class="box-content-row" *ngIf="cipher.sshKey.publicKey" style="overflow: hidden">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.sshKey.publicKey)"
>
{{ "sshPublicKey" | i18n }}</span
>
{{ cipher.sshKey.publicKey }}
</div>
<div class="box-content-row" *ngIf="cipher.sshKey.keyFingerprint" style="overflow: hidden">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.sshKey.keyFingerprint)"
>
{{ "sshFingerprint" | i18n }}</span
>
{{ cipher.sshKey.keyFingerprint }}
</div>
</div>
</div>
</div>
<div class="box" *ngIf="cipher.login && cipher.login.hasUris">
<div class="box-content">
<div
class="box-content-row box-content-row-flex"
*ngFor="let u of cipher.login.uris; let i = index"
>
<div class="row-main">
<label
for="hostOrUri{{ i }}"
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, u.uri)"
*ngIf="!u.isWebsite"
>{{ "uri" | i18n }}</label
>
<label
for="hostOrUri{{ i }}"
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, u.uri)"
*ngIf="u.isWebsite"
>{{ "website" | i18n }}</label
>
<span title="{{ u.uri }}">
<input
id="hostOrUri{{ i }}"
type="text"
[value]="u.hostOrUri"
readonly
aria-readonly="true"
/>
</span>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
attr.aria-label="{{ 'launch' | i18n }} {{ u.uri }}"
appA11yTitle="{{ 'launch' | i18n }}"
*ngIf="u.canLaunch"
(click)="launch(u, cipher.id)"
>
<i class="bwi bwi-lg bwi-share-square" aria-hidden="true"></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
attr.aria-label="{{ 'copyUri' | i18n }} {{ u.uri }}"
appA11yTitle="{{ 'copyUri' | i18n }}"
(click)="copy(u.uri, u.isWebsite ? 'website' : 'uri', 'URI')"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</div>
<div class="box" *ngIf="cipher.folderId && folder">
<div class="box-content">
<div class="box-content-row">
<label
for="folderName"
class="draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, folder.name)"
>{{ "folder" | i18n }}</label
>
<input id="folderName" type="text" name="folderName" [value]="folder.name" readonly />
</div>
</div>
</div>
<div class="box" *ngIf="cipher.notes">
<h2 class="box-header">
<label
for="notes"
class="draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.notes)"
>{{ "notes" | i18n }}</label
>
</h2>
<div class="box-content">
<div class="box-content-row">
<textarea
id="notes"
[value]="cipher.notes"
rows="6"
readonly
aria-readonly="true"
></textarea>
</div>
</div>
</div>
<div class="box" *ngIf="cipher.hasFields">
<app-vault-view-custom-fields
[cipher]="cipher"
[promptPassword]="promptPassword.bind(this)"
[copy]="copy.bind(this)"
></app-vault-view-custom-fields>
</div>
<div
class="box"
*ngIf="cipher.hasAttachments && (canAccessPremium || cipher.organizationId) && showAttachments"
>
<h2 class="box-header">
{{ "attachments" | i18n }}
</h2>
<div class="box-content single-line">
<button
type="button"
class="box-content-row box-content-row-flex text-default"
*ngFor="let attachment of cipher.attachments"
appStopClick
(click)="downloadAttachment(attachment)"
>
<span class="row-main">{{ attachment.fileName }}</span>
<small class="row-sub-label">{{ attachment.sizeName }}</small>
<i
class="bwi bwi-download bwi-fw row-sub-icon"
*ngIf="!$any(attachment).downloading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-fw bwi-spin row-sub-icon"
*ngIf="$any(attachment).downloading"
aria-hidden="true"
></i>
</button>
</div>
</div>
<div class="box list">
<div class="box-content single-line">
<button
type="button"
class="box-content-row"
appStopClick
(click)="fillCipher()"
*ngIf="
cipher.type !== cipherType.SecureNote &&
!cipher.isDeleted &&
(!this.inPopout || this.loadAction)
"
>
<div class="row-main text-primary">
<div class="icon text-primary" aria-hidden="true">
<i class="bwi bwi-pencil-square bwi-lg bwi-fw"></i>
</div>
<span>{{ "autoFill" | i18n }}</span>
</div>
</button>
<button
type="button"
class="box-content-row"
appStopClick
(click)="fillCipherAndSave()"
*ngIf="cipher.type === cipherType.Login && !cipher.isDeleted && !inPopout"
>
<div class="row-main text-primary">
<div class="icon text-primary" aria-hidden="true">
<i class="bwi bwi-bookmark bwi-lg bwi-fw"></i>
</div>
<span>{{ "autoFillAndSave" | i18n }}</span>
</div>
</button>
<button
type="button"
class="box-content-row"
appStopClick
(click)="clone()"
*ngIf="!cipher.organizationId && !cipher.isDeleted"
>
<div class="row-main text-primary">
<div class="icon text-primary" aria-hidden="true">
<i class="bwi bwi-files bwi-lg bwi-fw"></i>
</div>
<span>{{ "cloneItem" | i18n }}</span>
</div>
</button>
<button
type="button"
class="box-content-row"
appStopClick
(click)="share()"
*ngIf="!cipher.organizationId"
>
<div class="row-main text-primary">
<div class="icon text-primary" aria-hidden="true">
<i class="bwi bwi-arrow-circle-right bwi-lg bwi-fw"></i>
</div>
<span>{{ "moveToOrganization" | i18n }}</span>
</div>
</button>
<button
type="button"
class="box-content-row"
appStopClick
(click)="restore()"
*ngIf="cipher.isDeleted"
>
<div class="row-main text-primary">
<div class="icon text-primary" aria-hidden="true">
<i class="bwi bwi-undo bwi-lg bwi-fw"></i>
</div>
<span>{{ "restoreItem" | i18n }}</span>
</div>
</button>
<button
type="button"
class="box-content-row"
appStopClick
(click)="delete()"
*ngIf="canDeleteCipher$ | async"
>
<div class="row-main text-danger">
<div class="icon text-danger" aria-hidden="true">
<i class="bwi bwi-trash bwi-lg bwi-fw"></i>
</div>
<span>{{ (cipher.isDeleted ? "permanentlyDeleteItem" : "deleteItem") | i18n }}</span>
</div>
</button>
</div>
</div>
<div class="box">
<div class="box-footer">
<div>
<b class="font-weight-semibold">{{ "dateUpdated" | i18n }}:</b>
{{ cipher.revisionDate | date: "medium" }}
</div>
<div *ngIf="cipher.creationDate">
<b class="font-weight-semibold">{{ "dateCreated" | i18n }}:</b>
{{ cipher.creationDate | date: "medium" }}
</div>
<div *ngIf="cipher.passwordRevisionDisplayDate">
<b class="font-weight-semibold">{{ "datePasswordUpdated" | i18n }}:</b>
{{ cipher.passwordRevisionDisplayDate | date: "medium" }}
</div>
<div *ngIf="cipher.hasPasswordHistory">
<b class="font-weight-semibold">{{ "passwordHistory" | i18n }}:</b>
<button
type="button"
routerLink="/cipher-password-history"
[queryParams]="{ cipherId: cipher.id }"
appStopClick
title="{{ 'passwordHistory' | i18n }}"
>
{{ cipher.passwordHistory.length }}
</button>
</div>
</div>
</div>
</main>

View File

@@ -1,443 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DatePipe, Location } from "@angular/common";
import { ChangeDetectorRef, Component, NgZone, OnInit, OnDestroy } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { Subject, firstValueFrom, takeUntil, Subscription } from "rxjs";
import { first, map } from "rxjs/operators";
import { ViewComponent as BaseViewComponent } from "@bitwarden/angular/vault/components/view.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import { PasswordRepromptService } from "@bitwarden/vault";
import { BrowserFido2UserInterfaceSession } from "../../../../autofill/fido2/services/browser-fido2-user-interface.service";
import { AutofillService } from "../../../../autofill/services/abstractions/autofill.service";
import { BrowserApi } from "../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
import { fido2PopoutSessionData$ } from "../../utils/fido2-popout-session-data";
import { closeViewVaultItemPopout, VaultPopoutType } from "../../utils/vault-popout-window";
const BroadcasterSubscriptionId = "ChildViewComponent";
export const AUTOFILL_ID = "autofill";
export const SHOW_AUTOFILL_BUTTON = "show-autofill-button";
export const COPY_USERNAME_ID = "copy-username";
export const COPY_PASSWORD_ID = "copy-password";
export const COPY_VERIFICATION_CODE_ID = "copy-totp";
type CopyAction =
| typeof COPY_USERNAME_ID
| typeof COPY_PASSWORD_ID
| typeof COPY_VERIFICATION_CODE_ID;
type LoadAction = typeof AUTOFILL_ID | typeof SHOW_AUTOFILL_BUTTON | CopyAction;
@Component({
selector: "app-vault-view",
templateUrl: "view.component.html",
})
export class ViewComponent extends BaseViewComponent implements OnInit, OnDestroy {
showAttachments = true;
pageDetails: any[] = [];
tab: any;
senderTabId?: number;
loadAction?: LoadAction;
private static readonly copyActions = new Set([
COPY_USERNAME_ID,
COPY_PASSWORD_ID,
COPY_VERIFICATION_CODE_ID,
]);
uilocation?: "popout" | "popup" | "sidebar" | "tab";
loadPageDetailsTimeout: number;
inPopout = false;
cipherType = CipherType;
private fido2PopoutSessionData$ = fido2PopoutSessionData$();
private collectPageDetailsSubscription: Subscription;
private destroy$ = new Subject<void>();
constructor(
cipherService: CipherService,
folderService: FolderService,
totpService: TotpServiceAbstraction,
tokenService: TokenService,
i18nService: I18nService,
keyService: KeyService,
encryptService: EncryptService,
platformUtilsService: PlatformUtilsService,
auditService: AuditService,
private route: ActivatedRoute,
private router: Router,
private location: Location,
broadcasterService: BroadcasterService,
ngZone: NgZone,
changeDetectorRef: ChangeDetectorRef,
stateService: StateService,
eventCollectionService: EventCollectionService,
private autofillService: AutofillService,
private messagingService: MessagingService,
apiService: ApiService,
passwordRepromptService: PasswordRepromptService,
logService: LogService,
fileDownloadService: FileDownloadService,
dialogService: DialogService,
datePipe: DatePipe,
accountService: AccountService,
billingAccountProfileStateService: BillingAccountProfileStateService,
cipherAuthorizationService: CipherAuthorizationService,
) {
super(
cipherService,
folderService,
totpService,
tokenService,
i18nService,
keyService,
encryptService,
platformUtilsService,
auditService,
window,
broadcasterService,
ngZone,
changeDetectorRef,
eventCollectionService,
apiService,
passwordRepromptService,
logService,
stateService,
fileDownloadService,
dialogService,
datePipe,
accountService,
billingAccountProfileStateService,
cipherAuthorizationService,
);
}
ngOnInit() {
this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((value) => {
this.loadAction = value?.action;
this.senderTabId = parseInt(value?.senderTabId, 10) || undefined;
this.uilocation = value?.uilocation;
});
this.inPopout = this.uilocation === "popout" || BrowserPopupUtils.inPopout(window);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {
if (params.cipherId) {
this.cipherId = params.cipherId;
}
if (params.collectionId) {
this.collectionId = params.collectionId;
}
if (!params.cipherId) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.close();
}
await this.load();
});
super.ngOnInit();
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.ngZone.run(async () => {
switch (message.command) {
case "tabChanged":
case "windowChanged":
if (this.loadPageDetailsTimeout != null) {
window.clearTimeout(this.loadPageDetailsTimeout);
}
this.loadPageDetailsTimeout = window.setTimeout(() => this.loadPageDetails(), 500);
break;
default:
break;
}
});
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
super.ngOnDestroy();
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
async load() {
await super.load();
await this.loadPageDetails();
await this.handleLoadAction();
}
async edit() {
if (this.cipher.isDeleted) {
return false;
}
if (!(await super.edit())) {
return false;
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/edit-cipher"], {
queryParams: {
cipherId: this.cipher.id,
type: this.cipher.type,
isNew: false,
collectionId: this.collectionId,
},
});
return true;
}
async clone() {
if (this.cipher.isDeleted) {
return false;
}
if (!(await super.clone())) {
return false;
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/clone-cipher"], {
queryParams: {
cloneMode: true,
cipherId: this.cipher.id,
},
});
return true;
}
async share() {
if (!(await super.share())) {
return false;
}
if (this.cipher.organizationId == null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/share-cipher"], {
replaceUrl: true,
queryParams: { cipherId: this.cipher.id },
});
}
return true;
}
async fillCipher() {
const didAutofill = await this.doAutofill();
if (didAutofill) {
this.platformUtilsService.showToast("success", null, this.i18nService.t("autoFillSuccess"));
}
return didAutofill;
}
async fillCipherAndSave() {
const didAutofill = await this.doAutofill();
if (didAutofill) {
if (this.tab == null) {
throw new Error("No tab found.");
}
if (this.cipher.login.uris == null) {
this.cipher.login.uris = [];
} else {
if (this.cipher.login.uris.some((uri) => uri.uri === this.tab.url)) {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("autoFillSuccessAndSavedUri"),
);
return;
}
}
const loginUri = new LoginUriView();
loginUri.uri = this.tab.url;
this.cipher.login.uris.push(loginUri);
try {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const cipher: Cipher = await this.cipherService.encrypt(this.cipher, activeUserId);
await this.cipherService.updateWithServer(cipher);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("autoFillSuccessAndSavedUri"),
);
this.messagingService.send("editedCipher");
} catch {
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
}
}
}
async restore() {
if (!this.cipher.isDeleted) {
return false;
}
if (await super.restore()) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.close();
return true;
}
return false;
}
async delete() {
if (await super.delete()) {
this.messagingService.send("deletedCipher");
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.close();
return true;
}
return false;
}
async close() {
const sessionData = await firstValueFrom(this.fido2PopoutSessionData$);
if (this.inPopout && sessionData.isFido2Session) {
BrowserFido2UserInterfaceSession.abortPopout(sessionData.sessionId);
return;
}
if (
BrowserPopupUtils.inSingleActionPopout(window, VaultPopoutType.viewVaultItem) &&
this.senderTabId
) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserApi.focusTab(this.senderTabId);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
closeViewVaultItemPopout(`${VaultPopoutType.viewVaultItem}_${this.cipher.id}`);
return;
}
this.location.back();
}
private async loadPageDetails() {
this.collectPageDetailsSubscription?.unsubscribe();
this.pageDetails = [];
this.tab = this.senderTabId
? await BrowserApi.getTab(this.senderTabId)
: await BrowserApi.getTabFromCurrentWindow();
if (!this.tab) {
return;
}
this.collectPageDetailsSubscription = this.autofillService
.collectPageDetailsFromTab$(this.tab)
.pipe(takeUntil(this.destroy$))
.subscribe((pageDetails) => (this.pageDetails = pageDetails));
}
private async doAutofill() {
const originalTabURL = this.tab.url?.length && new URL(this.tab.url);
if (!(await this.promptPassword())) {
return false;
}
const currentTabURL = this.tab.url?.length && new URL(this.tab.url);
const originalTabHostPath =
originalTabURL && `${originalTabURL.origin}${originalTabURL.pathname}`;
const currentTabHostPath = currentTabURL && `${currentTabURL.origin}${currentTabURL.pathname}`;
const tabUrlChanged = originalTabHostPath !== currentTabHostPath;
if (this.pageDetails == null || this.pageDetails.length === 0 || tabUrlChanged) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError"));
return false;
}
try {
this.totpCode = await this.autofillService.doAutoFill({
tab: this.tab,
cipher: this.cipher,
pageDetails: this.pageDetails,
doc: window.document,
fillNewPassword: true,
allowTotpAutofill: true,
});
if (this.totpCode != null) {
this.platformUtilsService.copyToClipboard(this.totpCode, { window: window });
}
} catch {
this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError"));
this.changeDetectorRef.detectChanges();
return false;
}
return true;
}
private async handleLoadAction() {
if (!this.loadAction || this.loadAction === SHOW_AUTOFILL_BUTTON) {
return;
}
let loadActionSuccess = false;
if (this.loadAction === AUTOFILL_ID) {
loadActionSuccess = await this.fillCipher();
}
if (ViewComponent.copyActions.has(this.loadAction)) {
const { username, password } = this.cipher.login;
const copyParams: Record<CopyAction, Record<string, string>> = {
[COPY_USERNAME_ID]: { value: username, type: "username", name: "Username" },
[COPY_PASSWORD_ID]: { value: password, type: "password", name: "Password" },
[COPY_VERIFICATION_CODE_ID]: {
value: this.totpCode,
type: "verificationCodeTotp",
name: "TOTP",
},
};
const { value, type, name } = copyParams[this.loadAction as CopyAction];
loadActionSuccess = await this.copy(value, type, name);
}
if (this.inPopout) {
setTimeout(() => this.close(), loadActionSuccess ? 1000 : 0);
}
}
}

View File

@@ -1,80 +0,0 @@
<header>
<div class="left">
<button type="button" routerLink="/tabs/settings">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "appearance" | i18n }}</span>
</h1>
<div class="right">
<app-pop-out></app-pop-out>
</div>
</header>
<main tabindex="-1">
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="theme">{{ "theme" | i18n }}</label>
<select
id="theme"
name="Theme"
aria-describedby="themeHelp"
[(ngModel)]="theme"
(change)="saveTheme()"
>
<option *ngFor="let o of themeOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
</div>
<div id="themeHelp" class="box-footer">
{{ accountSwitcherEnabled ? ("themeDescAlt" | i18n) : ("themeDesc" | i18n) }}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="badge">{{ "enableBadgeCounter" | i18n }}</label>
<input
id="badge"
type="checkbox"
aria-describedby="badgeHelp"
(change)="updateBadgeCounter()"
[(ngModel)]="enableBadgeCounter"
/>
</div>
</div>
<div id="badgeHelp" class="box-footer">{{ "badgeCounterDesc" | i18n }}</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="favicon">{{ "enableFavicon" | i18n }}</label>
<input
id="favicon"
type="checkbox"
aria-describedby="faviconHelp"
(change)="updateFavicon()"
[(ngModel)]="enableFavicon"
/>
</div>
</div>
<div id="faviconHelp" class="box-footer">
{{ accountSwitcherEnabled ? ("faviconDescAlt" | i18n) : ("faviconDesc" | i18n) }}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="routing">{{ "enableAnimations" | i18n }}</label>
<input
id="routing"
type="checkbox"
(change)="updateRoutingAnimation()"
[(ngModel)]="enableRoutingAnimation"
/>
</div>
</div>
</div>
</main>

View File

@@ -1,75 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { enableAccountSwitching } from "../../../platform/flags";
@Component({
selector: "vault-appearance",
templateUrl: "appearance.component.html",
})
export class AppearanceComponent implements OnInit {
enableFavicon = false;
enableBadgeCounter = true;
theme: ThemeType;
themeOptions: any[];
accountSwitcherEnabled = false;
enableRoutingAnimation: boolean;
constructor(
private messagingService: MessagingService,
private domainSettingsService: DomainSettingsService,
private badgeSettingsService: BadgeSettingsServiceAbstraction,
i18nService: I18nService,
private themeStateService: ThemeStateService,
private animationControlService: AnimationControlService,
) {
this.themeOptions = [
{ name: i18nService.t("default"), value: ThemeType.System },
{ name: i18nService.t("light"), value: ThemeType.Light },
{ name: i18nService.t("dark"), value: ThemeType.Dark },
{ name: "Nord", value: ThemeType.Nord },
{ name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark },
];
this.accountSwitcherEnabled = enableAccountSwitching();
}
async ngOnInit() {
this.enableRoutingAnimation = await firstValueFrom(
this.animationControlService.enableRoutingAnimation$,
);
this.enableFavicon = await firstValueFrom(this.domainSettingsService.showFavicons$);
this.enableBadgeCounter = await firstValueFrom(this.badgeSettingsService.enableBadgeCounter$);
this.theme = await firstValueFrom(this.themeStateService.selectedTheme$);
}
async updateRoutingAnimation() {
await this.animationControlService.setEnableRoutingAnimation(this.enableRoutingAnimation);
}
async updateFavicon() {
await this.domainSettingsService.setShowFavicons(this.enableFavicon);
}
async updateBadgeCounter() {
await this.badgeSettingsService.setEnableBadgeCounter(this.enableBadgeCounter);
this.messagingService.send("bgUpdateContextMenu");
}
async saveTheme() {
await this.themeStateService.setSelectedTheme(this.theme);
}
}

View File

@@ -1,49 +0,0 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" [formGroup]="formGroup">
<header>
<div class="left">
<button type="button" routerLink="/folders">{{ "cancel" | i18n }}</button>
</div>
<h1 class="center">
<span class="title">{{ title }}</span>
</h1>
<div class="right">
<button type="submit" [disabled]="form.loading">
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
</div>
</header>
<main tabindex="-1" *ngIf="folder">
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="name">{{ "name" | i18n }}</label>
<input id="name" type="text" formControlName="name" [appAutofocus]="!editMode" />
</div>
</div>
</div>
<div class="box list" *ngIf="editMode">
<div class="box-content single-line">
<button
type="button"
class="box-content-row"
appStopClick
(click)="delete()"
[appApiAction]="deletePromise"
#deleteBtn
>
<div class="row-main text-danger">
<div class="icon text-danger" aria-hidden="true">
<i class="bwi bwi-trash bwi-lg bwi-fw" [hidden]="$any(deleteBtn).loading"></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!$any(deleteBtn).loading"
></i>
</div>
<span>{{ "deleteFolder" | i18n }}</span>
</div>
</button>
</div>
</div>
</main>
</form>

View File

@@ -1,78 +0,0 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { FolderAddEditComponent as BaseFolderAddEditComponent } from "@bitwarden/angular/vault/components/folder-add-edit.component";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { DialogService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
@Component({
selector: "app-folder-add-edit",
templateUrl: "folder-add-edit.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class FolderAddEditComponent extends BaseFolderAddEditComponent implements OnInit {
constructor(
folderService: FolderService,
folderApiService: FolderApiServiceAbstraction,
accountService: AccountService,
keyService: KeyService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
private router: Router,
private route: ActivatedRoute,
logService: LogService,
dialogService: DialogService,
formBuilder: FormBuilder,
) {
super(
folderService,
folderApiService,
accountService,
keyService,
i18nService,
platformUtilsService,
logService,
dialogService,
formBuilder,
);
}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {
if (params.folderId) {
this.folderId = params.folderId;
}
await this.init();
});
}
async submit(): Promise<boolean> {
if (await super.submit()) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/folders"]);
return true;
}
return false;
}
async delete(): Promise<boolean> {
const confirmed = await super.delete();
if (confirmed) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/folders"]);
}
return confirmed;
}
}

View File

@@ -1,38 +0,0 @@
<header>
<div class="left">
<button type="button" routerLink="/vault-settings">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "folders" | i18n }}</span>
</h1>
<div class="right">
<button type="button" (click)="addFolder()" appA11yTitle="{{ 'addFolder' | i18n }}">
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
</div>
</header>
<main tabindex="-1">
<ng-container *ngIf="folders$ | async as folders">
<div class="box list full-list" *ngIf="folders.length; else noFoldersTemplate">
<div class="box-content">
<button
type="button"
appStopClick
(click)="folderSelected(f)"
class="box-content-row padded"
*ngFor="let f of folders"
>
{{ f.name }}
</button>
</div>
</div>
</ng-container>
<ng-template #noFoldersTemplate>
<div class="no-items">
<p>{{ "noFolders" | i18n }}</p>
</div>
</ng-template>
</main>

View File

@@ -1,48 +0,0 @@
import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { filter, map, Observable, switchMap } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserId } from "@bitwarden/common/types/guid";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
@Component({
selector: "app-folders",
templateUrl: "folders.component.html",
})
export class FoldersComponent {
folders$: Observable<FolderView[]>;
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor(
private folderService: FolderService,
private router: Router,
private accountService: AccountService,
) {
this.folders$ = this.activeUserId$.pipe(
filter((userId): userId is UserId => userId != null),
switchMap((userId) => this.folderService.folderViews$(userId)),
map((folders) => {
// Remove the last folder, which is the "no folder" option folder
if (folders.length > 0) {
return folders.slice(0, folders.length - 1);
}
return folders;
}),
);
}
folderSelected(folder: FolderView) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/edit-folder"], { queryParams: { folderId: folder.id } });
}
addFolder() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/add-folder"]);
}
}

View File

@@ -1,35 +0,0 @@
<header>
<div class="left">
<button type="button" routerLink="/vault-settings">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "sync" | i18n }}</span>
</h1>
<div class="right"></div>
</header>
<main tabindex="-1">
<div class="content center-content">
<button
type="button"
class="btn block primary"
aria-describedby="lastSyncHint"
(click)="sync()"
#syncBtn
[disabled]="$any(syncBtn).loading"
[appApiAction]="syncPromise"
>
<span [hidden]="$any(syncBtn).loading">{{ "syncVaultNow" | i18n }}</span>
<i
class="bwi bwi-spinner bwi-lg bwi-spin"
[hidden]="!$any(syncBtn).loading"
aria-hidden="true"
></i>
</button>
<p id="lastSyncHint" class="text-center text-muted small">
{{ "lastSync" | i18n }} {{ lastSync }}
</p>
</div>
</main>

View File

@@ -1,46 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/platform/sync";
@Component({
selector: "app-sync",
templateUrl: "sync.component.html",
})
export class SyncComponent implements OnInit {
lastSync = "--";
syncPromise: Promise<any>;
constructor(
private syncService: SyncService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
) {}
async ngOnInit() {
await this.setLastSync();
}
async sync() {
this.syncPromise = this.syncService.fullSync(true);
const success = await this.syncPromise;
if (success) {
await this.setLastSync();
this.platformUtilsService.showToast("success", null, this.i18nService.t("syncingComplete"));
} else {
this.platformUtilsService.showToast("error", null, this.i18nService.t("syncingFailed"));
}
}
async setLastSync() {
const last = await this.syncService.getLastSync();
if (last != null) {
this.lastSync = last.toLocaleDateString() + " " + last.toLocaleTimeString();
} else {
this.lastSync = this.i18nService.t("never");
}
}
}

View File

@@ -1,56 +0,0 @@
<app-header>
<div class="left">
<button type="button" routerLink="/tabs/settings">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "vault" | i18n }}</span>
</h1>
<div class="right">
<app-pop-out></app-pop-out>
</div>
</app-header>
<main tabindex="-1">
<div class="box list">
<div class="box-content single-line">
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/folders"
>
<div class="row-main">{{ "folders" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="import()"
>
<div class="row-main">{{ "importItems" | i18n }}</div>
<i
class="bwi bwi-external-link bwi-lg row-sub-icon bwi-rotate-270 bwi-fw"
aria-hidden="true"
></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/export"
>
<div class="row-main">{{ "exportVault" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/sync"
>
<div class="row-main">{{ "sync" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
</div>
</div>
</main>

View File

@@ -1,25 +0,0 @@
import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { BrowserApi } from "../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
@Component({
selector: "vault-settings",
templateUrl: "vault-settings.component.html",
})
export class VaultSettingsComponent {
constructor(
public messagingService: MessagingService,
private router: Router,
) {}
async import() {
await this.router.navigate(["/import"]);
if (await BrowserApi.isPopupOpen()) {
await BrowserPopupUtils.openCurrentPagePopout(window);
}
}
}

View File

@@ -567,9 +567,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.4" version = "1.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333"
dependencies = [ dependencies = [
"shlex", "shlex",
] ]
@@ -1134,7 +1134,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -1519,7 +1519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-targets 0.52.6", "windows-targets 0.48.5",
] ]
[[package]] [[package]]
@@ -1667,9 +1667,9 @@ dependencies = [
[[package]] [[package]]
name = "napi-build" name = "napi-build"
version = "2.1.3" version = "2.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a" checksum = "db836caddef23662b94e16bf1f26c40eceb09d6aee5d5b06a7ac199320b69b19"
[[package]] [[package]]
name = "napi-derive" name = "napi-derive"
@@ -2836,7 +2836,7 @@ dependencies = [
"fastrand", "fastrand",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -3340,7 +3340,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.48.0",
] ]
[[package]] [[package]]

View File

@@ -21,10 +21,10 @@ serde = { version = "1.0.205", features = ["derive"] }
serde_json = "1.0.122" serde_json = "1.0.122"
tokio = { version = "1.39.2", features = ["sync"] } tokio = { version = "1.39.2", features = ["sync"] }
tokio-util = "0.7.11" tokio-util = "0.7.11"
uniffi = { version = "0.28.0", features = ["cli"] } uniffi = { version = "0.28.3", features = ["cli"] }
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
oslog = "0.2.0" oslog = "0.2.0"
[build-dependencies] [build-dependencies]
uniffi = { version = "0.28.0", features = ["build"] } uniffi = { version = "0.28.3", features = ["build"] }

View File

@@ -30,4 +30,4 @@ tokio-stream = "=0.1.15"
windows-registry = "=0.3.0" windows-registry = "=0.3.0"
[build-dependencies] [build-dependencies]
napi-build = "=2.1.3" napi-build = "=2.1.4"

View File

@@ -17,5 +17,5 @@ tokio = "1.39.1"
core-foundation = "=0.10.0" core-foundation = "=0.10.0"
[build-dependencies] [build-dependencies]
cc = "1.0.104" cc = "1.2.4"
glob = "0.3.1" glob = "0.3.1"

View File

@@ -3399,10 +3399,10 @@
"message": "ملاحظة هامة" "message": "ملاحظة هامة"
}, },
"setupTwoStepLogin": { "setupTwoStepLogin": {
"message": "إعداد المصادقة الثنائية" "message": "إعداد تسجيل الدخول بخطوتين"
}, },
"newDeviceVerificationNoticeContentPage1": { "newDeviceVerificationNoticeContentPage1": {
"message": "سيقوم Bitwarden بإرسال رمز إلى البريد الإلكتروني الخاص بحسابك للتحقق من تسجيلات الدخول من الأجهزة الجديدة ابتداء من فبراير 2025." "message": "سيقوم Bitwarden بإرسال رمز إلى البريد الإلكتروني الخاص بحسابك للتحقق من تسجيلات الدخول من الأجهزة الجديدة ابتداءً من فبراير 2025."
}, },
"newDeviceVerificationNoticeContentPage2": { "newDeviceVerificationNoticeContentPage2": {
"message": "يمكنك إعداد المصادقة الثنائية كطريقة بديلة لحماية حسابك أو تغيير بريدك الإلكتروني إلى بريد يمكنك الوصول إليه." "message": "يمكنك إعداد المصادقة الثنائية كطريقة بديلة لحماية حسابك أو تغيير بريدك الإلكتروني إلى بريد يمكنك الوصول إليه."
@@ -3426,7 +3426,7 @@
"message": "نعم، يمكنني الوصول بشكل موثوق إلى بريدي الإلكتروني" "message": "نعم، يمكنني الوصول بشكل موثوق إلى بريدي الإلكتروني"
}, },
"turnOnTwoStepLogin": { "turnOnTwoStepLogin": {
"message": فعيل المصادقة الثنائية" "message": شغيل تسجيل الدخول بخطوتين"
}, },
"changeAcctEmail": { "changeAcctEmail": {
"message": "تغيير البريد الإلكتروني الخاص بالحساب" "message": "تغيير البريد الإلكتروني الخاص بالحساب"

View File

@@ -2729,7 +2729,7 @@
"message": "Laitteeseesi lähetettiin ilmoitus" "message": "Laitteeseesi lähetettiin ilmoitus"
}, },
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" "message": "Varmista, että vahvistavan laitteen holvi on avattu ja että se näyttää saman tunnistelausekkeen"
}, },
"needAnotherOptionV1": { "needAnotherOptionV1": {
"message": "Tarvitsetko toisen vaihtoehdon?" "message": "Tarvitsetko toisen vaihtoehdon?"
@@ -3402,7 +3402,7 @@
"message": "Määritä kaksivaiheinen kirjautuminen" "message": "Määritä kaksivaiheinen kirjautuminen"
}, },
"newDeviceVerificationNoticeContentPage1": { "newDeviceVerificationNoticeContentPage1": {
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." "message": "Bitwarden lähettää tilisi sähköpostiosoitteeseen koodin, jolla voit vahvistaa kirjautumiset uusista laitteista helmikuusta 2025 alkaen."
}, },
"newDeviceVerificationNoticeContentPage2": { "newDeviceVerificationNoticeContentPage2": {
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."

View File

@@ -323,7 +323,7 @@
"message": "Générer un mot de passe" "message": "Générer un mot de passe"
}, },
"generatePassphrase": { "generatePassphrase": {
"message": "Generate passphrase" "message": "Générer une phrase de passe"
}, },
"type": { "type": {
"message": "Type" "message": "Type"
@@ -467,7 +467,7 @@
"message": "Copier la clé privée SSH" "message": "Copier la clé privée SSH"
}, },
"copyPassphrase": { "copyPassphrase": {
"message": "Copy passphrase", "message": "Copier la phrase de passe",
"description": "Copy passphrase to clipboard" "description": "Copy passphrase to clipboard"
}, },
"copyUri": { "copyUri": {
@@ -926,7 +926,7 @@
"message": "La session d'authentification a expiré. Veuillez redémarrer le processus de connexion." "message": "La session d'authentification a expiré. Veuillez redémarrer le processus de connexion."
}, },
"selfHostBaseUrl": { "selfHostBaseUrl": {
"message": "Self-host server URL", "message": "URL du serveur auto-hébergé",
"description": "Label for field requesting a self-hosted integration service URL" "description": "Label for field requesting a self-hosted integration service URL"
}, },
"apiUrl": { "apiUrl": {
@@ -1951,7 +1951,7 @@
"message": "Votre nouveau mot de passe principal ne répond pas aux exigences de politique de sécurité." "message": "Votre nouveau mot de passe principal ne répond pas aux exigences de politique de sécurité."
}, },
"receiveMarketingEmailsV2": { "receiveMarketingEmailsV2": {
"message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." "message": "Obtenez des conseils, des annonces et des opportunités de recherche de la part de Bitwarden dans votre boîte de réception."
}, },
"unsubscribe": { "unsubscribe": {
"message": "Se désabonner" "message": "Se désabonner"
@@ -2478,10 +2478,10 @@
"message": "Générer le nom d'utilisateur" "message": "Générer le nom d'utilisateur"
}, },
"generateEmail": { "generateEmail": {
"message": "Generate email" "message": "Générer un courriel"
}, },
"spinboxBoundariesHint": { "spinboxBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$.", "message": "La valeur doit être comprise entre $MIN$ et $MAX$.",
"description": "Explains spin box minimum and maximum values to the user", "description": "Explains spin box minimum and maximum values to the user",
"placeholders": { "placeholders": {
"min": { "min": {
@@ -2495,7 +2495,7 @@
} }
}, },
"passwordLengthRecommendationHint": { "passwordLengthRecommendationHint": {
"message": " Use $RECOMMENDED$ characters or more to generate a strong password.", "message": " Utilisez $RECOMMENDED$ caractères ou plus pour générer un mot de passe fort.",
"description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": { "placeholders": {
"recommended": { "recommended": {
@@ -2505,7 +2505,7 @@
} }
}, },
"passphraseNumWordsRecommendationHint": { "passphraseNumWordsRecommendationHint": {
"message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", "message": " Utilisez $RECOMMENDED$ mots ou plus pour générer une phrase de passe forte.",
"description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": { "placeholders": {
"recommended": { "recommended": {
@@ -2729,10 +2729,10 @@
"message": "A notification was sent to your device" "message": "A notification was sent to your device"
}, },
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" "message": "Assurez-vous que votre compte est déverrouillé et que la phrase d'empreinte digitale correspond à celle de l'autre appareil"
}, },
"needAnotherOptionV1": { "needAnotherOptionV1": {
"message": "Need another option?" "message": "Besoin d'une autre option ?"
}, },
"fingerprintMatchInfo": { "fingerprintMatchInfo": {
"message": "Veuillez vous assurer que votre coffre est déverrouillé et que la phrase d'empreinte correspond à celle de l'autre appareil." "message": "Veuillez vous assurer que votre coffre est déverrouillé et que la phrase d'empreinte correspond à celle de l'autre appareil."
@@ -2741,13 +2741,13 @@
"message": "Phrase d'empreinte" "message": "Phrase d'empreinte"
}, },
"youWillBeNotifiedOnceTheRequestIsApproved": { "youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved" "message": "Vous serez notifié une fois que la demande sera approuvée"
}, },
"needAnotherOption": { "needAnotherOption": {
"message": "La connexion avec l'appareil doit être configurée dans les paramètres de l'application Bitwarden. Besoin d'une autre option ?" "message": "La connexion avec l'appareil doit être configurée dans les paramètres de l'application Bitwarden. Besoin d'une autre option ?"
}, },
"viewAllLogInOptions": { "viewAllLogInOptions": {
"message": "View all log in options" "message": "Afficher toutes les options de connexion"
}, },
"viewAllLoginOptions": { "viewAllLoginOptions": {
"message": "Afficher toutes les options de connexion" "message": "Afficher toutes les options de connexion"

View File

@@ -27,7 +27,7 @@
"message": "Nota sicura" "message": "Nota sicura"
}, },
"typeSshKey": { "typeSshKey": {
"message": "SSH key" "message": "Chiave SSH"
}, },
"folders": { "folders": {
"message": "Cartelle" "message": "Cartelle"
@@ -64,7 +64,7 @@
} }
}, },
"welcomeBack": { "welcomeBack": {
"message": "Welcome back" "message": "Bentornato"
}, },
"moveToOrgDesc": { "moveToOrgDesc": {
"message": "Scegli un'organizzazione in cui vuoi spostare questo elemento. Spostarlo in un'organizzazione trasferisce la proprietà dell'elemento all'organizzazione. Una volta spostato, non sarai più il proprietario diretto di questo elemento." "message": "Scegli un'organizzazione in cui vuoi spostare questo elemento. Spostarlo in un'organizzazione trasferisce la proprietà dell'elemento all'organizzazione. Una volta spostato, non sarai più il proprietario diretto di questo elemento."
@@ -181,61 +181,61 @@
"message": "Indirizzo" "message": "Indirizzo"
}, },
"sshPrivateKey": { "sshPrivateKey": {
"message": "Private key" "message": "Chiave privata"
}, },
"sshPublicKey": { "sshPublicKey": {
"message": "Public key" "message": "Chiave pubblica"
}, },
"sshFingerprint": { "sshFingerprint": {
"message": "Fingerprint" "message": "Impronta digitale"
}, },
"sshKeyAlgorithm": { "sshKeyAlgorithm": {
"message": "Key type" "message": "Tipo di chiave"
}, },
"sshKeyAlgorithmED25519": { "sshKeyAlgorithmED25519": {
"message": "ED25519" "message": "ED25519"
}, },
"sshKeyAlgorithmRSA2048": { "sshKeyAlgorithmRSA2048": {
"message": "RSA 2048-Bit" "message": "RSA a 2048 bit"
}, },
"sshKeyAlgorithmRSA3072": { "sshKeyAlgorithmRSA3072": {
"message": "RSA 3072-Bit" "message": "RSA a 3072 bit"
}, },
"sshKeyAlgorithmRSA4096": { "sshKeyAlgorithmRSA4096": {
"message": "RSA 4096-Bit" "message": "RSA a 4096 bit"
}, },
"sshKeyGenerated": { "sshKeyGenerated": {
"message": "A new SSH key was generated" "message": "È stata generata una nuova chiave SSH"
}, },
"sshKeyWrongPassword": { "sshKeyWrongPassword": {
"message": "The password you entered is incorrect." "message": "La password inserita non è corretta."
}, },
"importSshKey": { "importSshKey": {
"message": "Import" "message": "Importa"
}, },
"confirmSshKeyPassword": { "confirmSshKeyPassword": {
"message": "Confirm password" "message": "Conferma password"
}, },
"enterSshKeyPasswordDesc": { "enterSshKeyPasswordDesc": {
"message": "Enter the password for the SSH key." "message": "Inserisci la password per la chiave SSH."
}, },
"enterSshKeyPassword": { "enterSshKeyPassword": {
"message": "Enter password" "message": "Inserisci password"
}, },
"sshAgentUnlockRequired": { "sshAgentUnlockRequired": {
"message": "Please unlock your vault to approve the SSH key request." "message": "Sbloccare la cassaforte per approvare la richiesta di chiave SSH."
}, },
"sshAgentUnlockTimeout": { "sshAgentUnlockTimeout": {
"message": "SSH key request timed out." "message": "Richiesta chiave SSH scaduta."
}, },
"enableSshAgent": { "enableSshAgent": {
"message": "Enable SSH agent" "message": "Abilita agente SSH"
}, },
"enableSshAgentDesc": { "enableSshAgentDesc": {
"message": "Enable the SSH agent to sign SSH requests right from your Bitwarden vault." "message": "Abilita l'agente SSH per firmare le richieste SSH direttamente dalla tua cassaforte Bitwarden."
}, },
"enableSshAgentHelp": { "enableSshAgentHelp": {
"message": "The SSH agent is a service targeted at developers that allows you to sign SSH requests directly from your Bitwarden vault." "message": "L'agente SSH è un servizio rivolto agli sviluppatori che consente di firmare le richieste SSH direttamente dalla tua cassaforte Bitwarden."
}, },
"premiumRequired": { "premiumRequired": {
"message": "Premium necessario" "message": "Premium necessario"
@@ -461,10 +461,10 @@
"message": "Copia password" "message": "Copia password"
}, },
"regenerateSshKey": { "regenerateSshKey": {
"message": "Regenerate SSH key" "message": "Rigenera la chiave SSH"
}, },
"copySshPrivateKey": { "copySshPrivateKey": {
"message": "Copy SSH private key" "message": "Copia chiave privata SSH"
}, },
"copyPassphrase": { "copyPassphrase": {
"message": "Copia passphrase", "message": "Copia passphrase",
@@ -624,7 +624,7 @@
"message": "Crea account" "message": "Crea account"
}, },
"newToBitwarden": { "newToBitwarden": {
"message": "New to Bitwarden?" "message": "Nuovo in Bitwarden?"
}, },
"setAStrongPassword": { "setAStrongPassword": {
"message": "Imposta una password robusta" "message": "Imposta una password robusta"
@@ -636,16 +636,16 @@
"message": "Accedi" "message": "Accedi"
}, },
"logInToBitwarden": { "logInToBitwarden": {
"message": "Log in to Bitwarden" "message": "Accedi a Bitwarden"
}, },
"logInWithPasskey": { "logInWithPasskey": {
"message": "Log in with passkey" "message": "Accedi con passkey"
}, },
"loginWithDevice": { "loginWithDevice": {
"message": "Log in with device" "message": "Accedi con dispositivo"
}, },
"useSingleSignOn": { "useSingleSignOn": {
"message": "Use single sign-on" "message": "Usa il Single Sign-On"
}, },
"submit": { "submit": {
"message": "Invia" "message": "Invia"
@@ -920,10 +920,10 @@
"message": "URL del server" "message": "URL del server"
}, },
"authenticationTimeout": { "authenticationTimeout": {
"message": "Authentication timeout" "message": "Timeout autenticazione"
}, },
"authenticationSessionTimedOut": { "authenticationSessionTimedOut": {
"message": "The authentication session timed out. Please restart the login process." "message": "La sessione di autenticazione è scaduta. Accedi di nuovo."
}, },
"selfHostBaseUrl": { "selfHostBaseUrl": {
"message": "URL server autogestito", "message": "URL server autogestito",
@@ -1393,13 +1393,13 @@
"message": "Cronologia delle password" "message": "Cronologia delle password"
}, },
"generatorHistory": { "generatorHistory": {
"message": "Generator history" "message": "Cronologia generatore"
}, },
"clearGeneratorHistoryTitle": { "clearGeneratorHistoryTitle": {
"message": "Clear generator history" "message": "Cancella cronologia generatore"
}, },
"cleargGeneratorHistoryDescription": { "cleargGeneratorHistoryDescription": {
"message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" "message": "Se continui, tutte le voci verranno eliminate definitivamente dalla cronologia del generatore. Vuoi continuare?"
}, },
"clear": { "clear": {
"message": "Cancella", "message": "Cancella",
@@ -1409,13 +1409,13 @@
"message": "Non ci sono password da mostrare." "message": "Non ci sono password da mostrare."
}, },
"clearHistory": { "clearHistory": {
"message": "Clear history" "message": "Cancella cronologia"
}, },
"nothingToShow": { "nothingToShow": {
"message": "Nothing to show" "message": "Niente da mostrare"
}, },
"nothingGeneratedRecently": { "nothingGeneratedRecently": {
"message": "You haven't generated anything recently" "message": "Non hai generato niente di recente"
}, },
"undo": { "undo": {
"message": "Annulla" "message": "Annulla"
@@ -1771,10 +1771,10 @@
"message": "L'eliminazione del tuo account è permanente. Non può essere annullata." "message": "L'eliminazione del tuo account è permanente. Non può essere annullata."
}, },
"cannotDeleteAccount": { "cannotDeleteAccount": {
"message": "Cannot delete account" "message": "Impossibile eliminare account"
}, },
"cannotDeleteAccountDesc": { "cannotDeleteAccountDesc": {
"message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details." "message": "Questa azione non può essere completata perché il tuo account è di proprietà di un'organizzazione. Contatta l'amministratore della tua organizzazione per dettagli."
}, },
"accountDeleted": { "accountDeleted": {
"message": "Account eliminato" "message": "Account eliminato"
@@ -2481,7 +2481,7 @@
"message": "Genera e-mail" "message": "Genera e-mail"
}, },
"spinboxBoundariesHint": { "spinboxBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$.", "message": "Il valore deve essere compreso tra $MIN$ e $MAX$.",
"description": "Explains spin box minimum and maximum values to the user", "description": "Explains spin box minimum and maximum values to the user",
"placeholders": { "placeholders": {
"min": { "min": {
@@ -2495,7 +2495,7 @@
} }
}, },
"passwordLengthRecommendationHint": { "passwordLengthRecommendationHint": {
"message": " Use $RECOMMENDED$ characters or more to generate a strong password.", "message": " Usa $RECOMMENDED$ caratteri o più per generare una password forte.",
"description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": { "placeholders": {
"recommended": { "recommended": {
@@ -2505,7 +2505,7 @@
} }
}, },
"passphraseNumWordsRecommendationHint": { "passphraseNumWordsRecommendationHint": {
"message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", "message": " Usa $RECOMMENDED$ parole o più per generare una passphrase forte.",
"description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": { "placeholders": {
"recommended": { "recommended": {
@@ -2726,13 +2726,13 @@
"message": "Una notifica è stata inviata al tuo dispositivo." "message": "Una notifica è stata inviata al tuo dispositivo."
}, },
"aNotificationWasSentToYourDevice": { "aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device" "message": "Una notifica è stata inviata al tuo dispositivo"
}, },
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" "message": "Assicurati che il tuo account sia sbloccato e che la frase dell'impronta digitale corrisponda nell'altro dispositivo"
}, },
"needAnotherOptionV1": { "needAnotherOptionV1": {
"message": "Need another option?" "message": "Bisogno di un'altra opzione?"
}, },
"fingerprintMatchInfo": { "fingerprintMatchInfo": {
"message": "Assicurati che la tua cassaforte sia sbloccata e che la frase impronta corrisponda sull'altro dispositivo." "message": "Assicurati che la tua cassaforte sia sbloccata e che la frase impronta corrisponda sull'altro dispositivo."
@@ -2741,13 +2741,13 @@
"message": "Frase impronta" "message": "Frase impronta"
}, },
"youWillBeNotifiedOnceTheRequestIsApproved": { "youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved" "message": "Sarai notificato una volta che la richiesta sarà approvata"
}, },
"needAnotherOption": { "needAnotherOption": {
"message": "L'accesso con dispositivo deve essere abilitato nelle impostazioni dell'app Bitwarden. Ti serve un'altra opzione?" "message": "L'accesso con dispositivo deve essere abilitato nelle impostazioni dell'app Bitwarden. Ti serve un'altra opzione?"
}, },
"viewAllLogInOptions": { "viewAllLogInOptions": {
"message": "View all log in options" "message": "Visualizza tutte le opzioni di accesso"
}, },
"viewAllLoginOptions": { "viewAllLoginOptions": {
"message": "Visualizza tutte le opzioni di accesso" "message": "Visualizza tutte le opzioni di accesso"
@@ -2869,7 +2869,7 @@
"message": "Controlla se la tua password è presente in una violazione dei dati" "message": "Controlla se la tua password è presente in una violazione dei dati"
}, },
"loggedInExclamation": { "loggedInExclamation": {
"message": "Logged in!" "message": "Accesso effettuato!"
}, },
"important": { "important": {
"message": "Importante:" "message": "Importante:"
@@ -2902,16 +2902,16 @@
"message": "Aggiornamento delle impostazioni consigliato" "message": "Aggiornamento delle impostazioni consigliato"
}, },
"rememberThisDeviceToMakeFutureLoginsSeamless": { "rememberThisDeviceToMakeFutureLoginsSeamless": {
"message": "Remember this device to make future logins seamless" "message": "Ricorda questo dispositivo per rendere immediati i futuri accessi"
}, },
"deviceApprovalRequired": { "deviceApprovalRequired": {
"message": "Approvazione del dispositivo obbligatoria. Seleziona un'opzione di approvazione:" "message": "Approvazione del dispositivo obbligatoria. Seleziona un'opzione di approvazione:"
}, },
"deviceApprovalRequiredV2": { "deviceApprovalRequiredV2": {
"message": "Device approval required" "message": "Approvazione dispositivo richiesta"
}, },
"selectAnApprovalOptionBelow": { "selectAnApprovalOptionBelow": {
"message": "Select an approval option below" "message": "Seleziona un'opzione di approvazione sotto"
}, },
"rememberThisDevice": { "rememberThisDevice": {
"message": "Ricorda questo dispositivo" "message": "Ricorda questo dispositivo"
@@ -2966,7 +2966,7 @@
"message": "Email utente mancante" "message": "Email utente mancante"
}, },
"activeUserEmailNotFoundLoggingYouOut": { "activeUserEmailNotFoundLoggingYouOut": {
"message": "Active user email not found. Logging you out." "message": "Email utente attiva non trovata. Logout in corso."
}, },
"deviceTrusted": { "deviceTrusted": {
"message": "Dispositivo fidato" "message": "Dispositivo fidato"
@@ -3363,55 +3363,55 @@
"message": "Non è stato possibile trovare nessuna porta libera per il login Sso." "message": "Non è stato possibile trovare nessuna porta libera per il login Sso."
}, },
"authorize": { "authorize": {
"message": "Authorize" "message": "Autorizza"
}, },
"deny": { "deny": {
"message": "Deny" "message": "Nega"
}, },
"sshkeyApprovalTitle": { "sshkeyApprovalTitle": {
"message": "Confirm SSH key usage" "message": "Conferma l'uso della chiave SSH"
}, },
"sshkeyApprovalMessageInfix": { "sshkeyApprovalMessageInfix": {
"message": "is requesting access to" "message": "richiede l'accesso a"
}, },
"unknownApplication": { "unknownApplication": {
"message": "An application" "message": "Un'applicazione"
}, },
"sshKeyPasswordUnsupported": { "sshKeyPasswordUnsupported": {
"message": "Importing password protected SSH keys is not yet supported" "message": "L'importazione di chiavi SSH protette da password non è ancora supportata"
}, },
"invalidSshKey": { "invalidSshKey": {
"message": "The SSH key is invalid" "message": "La chiave SSH non è valida"
}, },
"sshKeyTypeUnsupported": { "sshKeyTypeUnsupported": {
"message": "The SSH key type is not supported" "message": "Il tipo di chiave SSH non è supportato"
}, },
"importSshKeyFromClipboard": { "importSshKeyFromClipboard": {
"message": "Import key from clipboard" "message": "Importa chiave dagli Appunti"
}, },
"sshKeyPasted": { "sshKeyPasted": {
"message": "SSH key imported successfully" "message": "Chiave SSH importata correttamente"
}, },
"fileSavedToDevice": { "fileSavedToDevice": {
"message": "File salvato sul dispositivo. Gestisci dai download del dispositivo." "message": "File salvato sul dispositivo. Gestisci dai download del dispositivo."
}, },
"importantNotice": { "importantNotice": {
"message": "Important notice" "message": "Notifica importante"
}, },
"setupTwoStepLogin": { "setupTwoStepLogin": {
"message": "Set up two-step login" "message": "Imposta accesso in due passaggi"
}, },
"newDeviceVerificationNoticeContentPage1": { "newDeviceVerificationNoticeContentPage1": {
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." "message": "Bitwarden invierà un codice all'e-mail del tuo account per verificare gli accessi da nuovi dispositivi a partire da febbraio 2025."
}, },
"newDeviceVerificationNoticeContentPage2": { "newDeviceVerificationNoticeContentPage2": {
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." "message": "Puoi impostare l'accesso in due passaggi come modo alternativo per proteggere il tuo account, o cambiare la tua e-mail in una alla quale puoi accedere."
}, },
"remindMeLater": { "remindMeLater": {
"message": "Remind me later" "message": "Ricordamelo più tardi"
}, },
"newDeviceVerificationNoticePageOneFormContent": { "newDeviceVerificationNoticePageOneFormContent": {
"message": "Do you have reliable access to your email, $EMAIL$?", "message": "Hai accesso affidabile alla tua e-mail, $EMAIL$?",
"placeholders": { "placeholders": {
"email": { "email": {
"content": "$1", "content": "$1",
@@ -3420,15 +3420,15 @@
} }
}, },
"newDeviceVerificationNoticePageOneEmailAccessNo": { "newDeviceVerificationNoticePageOneEmailAccessNo": {
"message": "No, I do not" "message": "No, non ce l'ho"
}, },
"newDeviceVerificationNoticePageOneEmailAccessYes": { "newDeviceVerificationNoticePageOneEmailAccessYes": {
"message": "Yes, I can reliably access my email" "message": "Sì, posso accedere in modo affidabile alla mia e-mail"
}, },
"turnOnTwoStepLogin": { "turnOnTwoStepLogin": {
"message": "Turn on two-step login" "message": "Attiva accesso in due passaggi"
}, },
"changeAcctEmail": { "changeAcctEmail": {
"message": "Change account email" "message": "Cambia l'e-mail dell'account"
} }
} }

View File

@@ -27,7 +27,7 @@
"message": "セキュアメモ" "message": "セキュアメモ"
}, },
"typeSshKey": { "typeSshKey": {
"message": "SSH key" "message": "SSH キー"
}, },
"folders": { "folders": {
"message": "フォルダー" "message": "フォルダー"
@@ -64,7 +64,7 @@
} }
}, },
"welcomeBack": { "welcomeBack": {
"message": "Welcome back" "message": "ようこそ"
}, },
"moveToOrgDesc": { "moveToOrgDesc": {
"message": "このアイテムを移動する組織を選択してください。組織に移動すると、アイテムの所有権がその組織に移行します。 このアイテムが移動された後、あなたはこのアイテムの直接の所有者にはなりません。" "message": "このアイテムを移動する組織を選択してください。組織に移動すると、アイテムの所有権がその組織に移行します。 このアイテムが移動された後、あなたはこのアイテムの直接の所有者にはなりません。"
@@ -181,16 +181,16 @@
"message": "住所" "message": "住所"
}, },
"sshPrivateKey": { "sshPrivateKey": {
"message": "Private key" "message": "秘密鍵"
}, },
"sshPublicKey": { "sshPublicKey": {
"message": "Public key" "message": "公開鍵"
}, },
"sshFingerprint": { "sshFingerprint": {
"message": "Fingerprint" "message": "フィンガープリント"
}, },
"sshKeyAlgorithm": { "sshKeyAlgorithm": {
"message": "Key type" "message": "キーの種類"
}, },
"sshKeyAlgorithmED25519": { "sshKeyAlgorithmED25519": {
"message": "ED25519" "message": "ED25519"
@@ -205,37 +205,37 @@
"message": "RSA 4096-Bit" "message": "RSA 4096-Bit"
}, },
"sshKeyGenerated": { "sshKeyGenerated": {
"message": "A new SSH key was generated" "message": "新しい SSH 鍵が生成されました"
}, },
"sshKeyWrongPassword": { "sshKeyWrongPassword": {
"message": "The password you entered is incorrect." "message": "入力されたパスワードが間違っています。"
}, },
"importSshKey": { "importSshKey": {
"message": "Import" "message": "インポート"
}, },
"confirmSshKeyPassword": { "confirmSshKeyPassword": {
"message": "Confirm password" "message": "パスワードを確認"
}, },
"enterSshKeyPasswordDesc": { "enterSshKeyPasswordDesc": {
"message": "Enter the password for the SSH key." "message": "SSH キーのパスワードを入力します。"
}, },
"enterSshKeyPassword": { "enterSshKeyPassword": {
"message": "Enter password" "message": "パスワードを入力"
}, },
"sshAgentUnlockRequired": { "sshAgentUnlockRequired": {
"message": "Please unlock your vault to approve the SSH key request." "message": "SSH キーリクエストを承認するには、保管庫のロックを解除してください。"
}, },
"sshAgentUnlockTimeout": { "sshAgentUnlockTimeout": {
"message": "SSH key request timed out." "message": "SSH キーの要求がタイムアウトしました。"
}, },
"enableSshAgent": { "enableSshAgent": {
"message": "Enable SSH agent" "message": "SSH エージェントを有効にする"
}, },
"enableSshAgentDesc": { "enableSshAgentDesc": {
"message": "Enable the SSH agent to sign SSH requests right from your Bitwarden vault." "message": "Bitwarden 保管庫から直接 SSH 要求に署名するために SSH エージェントを有効にします。"
}, },
"enableSshAgentHelp": { "enableSshAgentHelp": {
"message": "The SSH agent is a service targeted at developers that allows you to sign SSH requests directly from your Bitwarden vault." "message": "SSH エージェントとは、Bitwarden 保管庫から直接 SSH リクエストに署名できる、開発者を対象としたサービスです。"
}, },
"premiumRequired": { "premiumRequired": {
"message": "プレミアム会員専用" "message": "プレミアム会員専用"
@@ -323,7 +323,7 @@
"message": "パスワードの自動生成" "message": "パスワードの自動生成"
}, },
"generatePassphrase": { "generatePassphrase": {
"message": "Generate passphrase" "message": "パスフレーズを生成"
}, },
"type": { "type": {
"message": "タイプ" "message": "タイプ"
@@ -461,13 +461,13 @@
"message": "パスワードのコピー" "message": "パスワードのコピー"
}, },
"regenerateSshKey": { "regenerateSshKey": {
"message": "Regenerate SSH key" "message": "SSH キーを再生成"
}, },
"copySshPrivateKey": { "copySshPrivateKey": {
"message": "Copy SSH private key" "message": "SSH 秘密鍵をコピー"
}, },
"copyPassphrase": { "copyPassphrase": {
"message": "Copy passphrase", "message": "パスフレーズをコピー",
"description": "Copy passphrase to clipboard" "description": "Copy passphrase to clipboard"
}, },
"copyUri": { "copyUri": {
@@ -624,7 +624,7 @@
"message": "アカウントの作成" "message": "アカウントの作成"
}, },
"newToBitwarden": { "newToBitwarden": {
"message": "New to Bitwarden?" "message": "Bitwarden は初めてですか?"
}, },
"setAStrongPassword": { "setAStrongPassword": {
"message": "強力なパスワードを設定する" "message": "強力なパスワードを設定する"
@@ -636,16 +636,16 @@
"message": "ログイン" "message": "ログイン"
}, },
"logInToBitwarden": { "logInToBitwarden": {
"message": "Log in to Bitwarden" "message": "Bitwarden にログイン"
}, },
"logInWithPasskey": { "logInWithPasskey": {
"message": "Log in with passkey" "message": "パスキーでログイン"
}, },
"loginWithDevice": { "loginWithDevice": {
"message": "Log in with device" "message": "デバイスでログイン"
}, },
"useSingleSignOn": { "useSingleSignOn": {
"message": "Use single sign-on" "message": "シングルサインオンを使用する"
}, },
"submit": { "submit": {
"message": "送信" "message": "送信"
@@ -920,13 +920,13 @@
"message": "サーバー URL" "message": "サーバー URL"
}, },
"authenticationTimeout": { "authenticationTimeout": {
"message": "Authentication timeout" "message": "認証のタイムアウト"
}, },
"authenticationSessionTimedOut": { "authenticationSessionTimedOut": {
"message": "The authentication session timed out. Please restart the login process." "message": "認証セッションの有効期限が切れました。ログイン操作を最初からやり直してください。"
}, },
"selfHostBaseUrl": { "selfHostBaseUrl": {
"message": "Self-host server URL", "message": "自己ホスト型サーバーの URL",
"description": "Label for field requesting a self-hosted integration service URL" "description": "Label for field requesting a self-hosted integration service URL"
}, },
"apiUrl": { "apiUrl": {
@@ -1320,7 +1320,7 @@
"description": "Copy credit card number" "description": "Copy credit card number"
}, },
"copyEmail": { "copyEmail": {
"message": "Copy email" "message": "メールアドレスをコピー"
}, },
"copySecurityCode": { "copySecurityCode": {
"message": "セキュリティコードのコピー", "message": "セキュリティコードのコピー",
@@ -1393,13 +1393,13 @@
"message": "パスワードの履歴" "message": "パスワードの履歴"
}, },
"generatorHistory": { "generatorHistory": {
"message": "Generator history" "message": "生成履歴"
}, },
"clearGeneratorHistoryTitle": { "clearGeneratorHistoryTitle": {
"message": "Clear generator history" "message": "生成履歴を消去"
}, },
"cleargGeneratorHistoryDescription": { "cleargGeneratorHistoryDescription": {
"message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" "message": "続行すると、すべてのエントリは生成履歴から完全に削除されます。続行してもよろしいですか?"
}, },
"clear": { "clear": {
"message": "消去する", "message": "消去する",
@@ -1409,13 +1409,13 @@
"message": "表示するパスワードがありません" "message": "表示するパスワードがありません"
}, },
"clearHistory": { "clearHistory": {
"message": "Clear history" "message": "履歴を消去"
}, },
"nothingToShow": { "nothingToShow": {
"message": "Nothing to show" "message": "表示するものがありません"
}, },
"nothingGeneratedRecently": { "nothingGeneratedRecently": {
"message": "You haven't generated anything recently" "message": "最近生成したものはありません"
}, },
"undo": { "undo": {
"message": "元に戻す" "message": "元に戻す"
@@ -1771,10 +1771,10 @@
"message": "アカウントを恒久的に削除します。元に戻すことはできません。" "message": "アカウントを恒久的に削除します。元に戻すことはできません。"
}, },
"cannotDeleteAccount": { "cannotDeleteAccount": {
"message": "Cannot delete account" "message": "アカウントを削除できません"
}, },
"cannotDeleteAccountDesc": { "cannotDeleteAccountDesc": {
"message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details." "message": "このアカウントは組織が所有しているため、操作を完了できません。詳しくは組織の管理者へご確認ください。"
}, },
"accountDeleted": { "accountDeleted": {
"message": "アカウントが削除されました" "message": "アカウントが削除されました"
@@ -2478,10 +2478,10 @@
"message": "ユーザー名を生成" "message": "ユーザー名を生成"
}, },
"generateEmail": { "generateEmail": {
"message": "Generate email" "message": "メールアドレスを生成"
}, },
"spinboxBoundariesHint": { "spinboxBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$.", "message": "値は $MIN$ から $MAX$ の間でなければなりません。",
"description": "Explains spin box minimum and maximum values to the user", "description": "Explains spin box minimum and maximum values to the user",
"placeholders": { "placeholders": {
"min": { "min": {
@@ -2495,7 +2495,7 @@
} }
}, },
"passwordLengthRecommendationHint": { "passwordLengthRecommendationHint": {
"message": " Use $RECOMMENDED$ characters or more to generate a strong password.", "message": " 強力なパスワードを生成するには、 $RECOMMENDED$ 文字以上を使用してください。",
"description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": { "placeholders": {
"recommended": { "recommended": {
@@ -2505,7 +2505,7 @@
} }
}, },
"passphraseNumWordsRecommendationHint": { "passphraseNumWordsRecommendationHint": {
"message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", "message": " 強力なパスフレーズを生成するには、 $RECOMMENDED$ 単語以上を使用してください。",
"description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": { "placeholders": {
"recommended": { "recommended": {
@@ -2558,11 +2558,11 @@
"message": "外部転送サービスを使用してメールエイリアスを生成します。" "message": "外部転送サービスを使用してメールエイリアスを生成します。"
}, },
"forwarderDomainName": { "forwarderDomainName": {
"message": "Email domain", "message": "メールアドレスのドメイン",
"description": "Labels the domain name email forwarder service option" "description": "Labels the domain name email forwarder service option"
}, },
"forwarderDomainNameHint": { "forwarderDomainNameHint": {
"message": "Choose a domain that is supported by the selected service", "message": "選択したサービスでサポートされているドメインを選択してください",
"description": "Guidance provided for email forwarding services that support multiple email domains." "description": "Guidance provided for email forwarding services that support multiple email domains."
}, },
"forwarderError": { "forwarderError": {
@@ -2726,13 +2726,13 @@
"message": "デバイスに通知を送信しました。" "message": "デバイスに通知を送信しました。"
}, },
"aNotificationWasSentToYourDevice": { "aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device" "message": "お使いのデバイスに通知が送信されました"
}, },
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" "message": "アカウントがロック解除されていることと、フィンガープリントフレーズが他の端末と一致していることを確認してください"
}, },
"needAnotherOptionV1": { "needAnotherOptionV1": {
"message": "Need another option?" "message": "別の選択肢が必要ですか?"
}, },
"fingerprintMatchInfo": { "fingerprintMatchInfo": {
"message": "保管庫がロックされていることと、パスフレーズが他のデバイスと一致していることを確認してください。" "message": "保管庫がロックされていることと、パスフレーズが他のデバイスと一致していることを確認してください。"
@@ -2741,13 +2741,13 @@
"message": "パスフレーズ" "message": "パスフレーズ"
}, },
"youWillBeNotifiedOnceTheRequestIsApproved": { "youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved" "message": "リクエストが承認されると通知されます"
}, },
"needAnotherOption": { "needAnotherOption": {
"message": "Bitwarden アプリの設定でデバイスでログインする必要があります。別のオプションが必要ですか?" "message": "Bitwarden アプリの設定でデバイスでログインする必要があります。別のオプションが必要ですか?"
}, },
"viewAllLogInOptions": { "viewAllLogInOptions": {
"message": "View all log in options" "message": "すべてのログインオプションを表示"
}, },
"viewAllLoginOptions": { "viewAllLoginOptions": {
"message": "すべてのログインオプションを表示" "message": "すべてのログインオプションを表示"
@@ -2869,7 +2869,7 @@
"message": "このパスワードの既知のデータ流出を確認" "message": "このパスワードの既知のデータ流出を確認"
}, },
"loggedInExclamation": { "loggedInExclamation": {
"message": "Logged in!" "message": "ログインしました!"
}, },
"important": { "important": {
"message": "重要" "message": "重要"
@@ -2902,16 +2902,16 @@
"message": "設定の更新を推奨" "message": "設定の更新を推奨"
}, },
"rememberThisDeviceToMakeFutureLoginsSeamless": { "rememberThisDeviceToMakeFutureLoginsSeamless": {
"message": "Remember this device to make future logins seamless" "message": "このデバイスを記憶して今後のログインをシームレスにする"
}, },
"deviceApprovalRequired": { "deviceApprovalRequired": {
"message": "デバイスの承認が必要です。以下から承認オプションを選択してください:" "message": "デバイスの承認が必要です。以下から承認オプションを選択してください:"
}, },
"deviceApprovalRequiredV2": { "deviceApprovalRequiredV2": {
"message": "Device approval required" "message": "デバイスの承認が必要です"
}, },
"selectAnApprovalOptionBelow": { "selectAnApprovalOptionBelow": {
"message": "Select an approval option below" "message": "以下の承認オプションを選択してください"
}, },
"rememberThisDevice": { "rememberThisDevice": {
"message": "このデバイスを記憶する" "message": "このデバイスを記憶する"
@@ -2966,7 +2966,7 @@
"message": "ユーザーのメールアドレスがありません" "message": "ユーザーのメールアドレスがありません"
}, },
"activeUserEmailNotFoundLoggingYouOut": { "activeUserEmailNotFoundLoggingYouOut": {
"message": "Active user email not found. Logging you out." "message": "アクティブなユーザーメールアドレスが見つかりません。ログアウトします。"
}, },
"deviceTrusted": { "deviceTrusted": {
"message": "信頼されたデバイス" "message": "信頼されたデバイス"
@@ -3363,55 +3363,55 @@
"message": "SSO ログインのための空きポートが見つかりませんでした。" "message": "SSO ログインのための空きポートが見つかりませんでした。"
}, },
"authorize": { "authorize": {
"message": "Authorize" "message": "認可"
}, },
"deny": { "deny": {
"message": "Deny" "message": "拒否"
}, },
"sshkeyApprovalTitle": { "sshkeyApprovalTitle": {
"message": "Confirm SSH key usage" "message": "SSH 鍵の使用を確認します"
}, },
"sshkeyApprovalMessageInfix": { "sshkeyApprovalMessageInfix": {
"message": "is requesting access to" "message": "がアクセスを要求しています: "
}, },
"unknownApplication": { "unknownApplication": {
"message": "An application" "message": "アプリ"
}, },
"sshKeyPasswordUnsupported": { "sshKeyPasswordUnsupported": {
"message": "Importing password protected SSH keys is not yet supported" "message": "パスワードで保護された SSH キーのインポートはまだサポートされていません"
}, },
"invalidSshKey": { "invalidSshKey": {
"message": "The SSH key is invalid" "message": "SSH キーが無効です"
}, },
"sshKeyTypeUnsupported": { "sshKeyTypeUnsupported": {
"message": "The SSH key type is not supported" "message": "サポートされていない種類の SSH キーです"
}, },
"importSshKeyFromClipboard": { "importSshKeyFromClipboard": {
"message": "Import key from clipboard" "message": "クリップボードからキーをインポート"
}, },
"sshKeyPasted": { "sshKeyPasted": {
"message": "SSH key imported successfully" "message": "SSH キーのインポートに成功しました"
}, },
"fileSavedToDevice": { "fileSavedToDevice": {
"message": "ファイルをデバイスに保存しました。デバイスのダウンロードで管理できます。" "message": "ファイルをデバイスに保存しました。デバイスのダウンロードで管理できます。"
}, },
"importantNotice": { "importantNotice": {
"message": "Important notice" "message": "重要なお知らせ"
}, },
"setupTwoStepLogin": { "setupTwoStepLogin": {
"message": "Set up two-step login" "message": "2段階認証によるログインを設定する"
}, },
"newDeviceVerificationNoticeContentPage1": { "newDeviceVerificationNoticeContentPage1": {
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." "message": "Bitwarden は2025年2月以降、新しいデバイスからのログイン時にアカウントのメールアドレスに確認コードを送信します。"
}, },
"newDeviceVerificationNoticeContentPage2": { "newDeviceVerificationNoticeContentPage2": {
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." "message": "代わりに2段階認証によるログインでアカウントを保護するか、メールアドレスをあなたがアクセスできるものに変更できます。"
}, },
"remindMeLater": { "remindMeLater": {
"message": "Remind me later" "message": "後で再通知"
}, },
"newDeviceVerificationNoticePageOneFormContent": { "newDeviceVerificationNoticePageOneFormContent": {
"message": "Do you have reliable access to your email, $EMAIL$?", "message": "新しいメールアドレス $EMAIL$ はあなたが管理しているものですか?",
"placeholders": { "placeholders": {
"email": { "email": {
"content": "$1", "content": "$1",
@@ -3420,15 +3420,15 @@
} }
}, },
"newDeviceVerificationNoticePageOneEmailAccessNo": { "newDeviceVerificationNoticePageOneEmailAccessNo": {
"message": "No, I do not" "message": "いいえ、違います"
}, },
"newDeviceVerificationNoticePageOneEmailAccessYes": { "newDeviceVerificationNoticePageOneEmailAccessYes": {
"message": "Yes, I can reliably access my email" "message": "はい、メールアドレスには私が確実にアクセスできます"
}, },
"turnOnTwoStepLogin": { "turnOnTwoStepLogin": {
"message": "Turn on two-step login" "message": "2段階認証によるログインを有効にする"
}, },
"changeAcctEmail": { "changeAcctEmail": {
"message": "Change account email" "message": "アカウントのメールアドレスを変更する"
} }
} }

View File

@@ -945,7 +945,7 @@
"message": "Pictogrammenserver-URL" "message": "Pictogrammenserver-URL"
}, },
"environmentSaved": { "environmentSaved": {
"message": "De omgevings-URL's zijn opgeslagen." "message": "Omgevings-URL's opgeslagen"
}, },
"ok": { "ok": {
"message": "Ok" "message": "Ok"
@@ -1002,7 +1002,7 @@
"message": "Nieuwe map toevoegen" "message": "Nieuwe map toevoegen"
}, },
"view": { "view": {
"message": "Beeld" "message": "Weergeven"
}, },
"account": { "account": {
"message": "Account" "message": "Account"
@@ -1268,7 +1268,7 @@
"description": "Copy to clipboard" "description": "Copy to clipboard"
}, },
"checkForUpdates": { "checkForUpdates": {
"message": "Controleren op updates" "message": "Controleren op updates"
}, },
"version": { "version": {
"message": "Versie $VERSION_NUM$", "message": "Versie $VERSION_NUM$",
@@ -3026,7 +3026,7 @@
} }
}, },
"multipleInputEmails": { "multipleInputEmails": {
"message": "Een of meer e-mailadressen zijn ongeldig" "message": "Eén of meer e-mailadressen zijn ongeldig"
}, },
"inputTrimValidator": { "inputTrimValidator": {
"message": "Invoer mag niet alleen witruimte bevatten.", "message": "Invoer mag niet alleen witruimte bevatten.",
@@ -3051,7 +3051,7 @@
"message": "-- Type om te filteren --" "message": "-- Type om te filteren --"
}, },
"multiSelectLoading": { "multiSelectLoading": {
"message": "Opties ophalen..." "message": "Opties ophalen"
}, },
"multiSelectNotFound": { "multiSelectNotFound": {
"message": "Geen items gevonden" "message": "Geen items gevonden"
@@ -3243,7 +3243,7 @@
"message": "LastPass Email" "message": "LastPass Email"
}, },
"importingYourAccount": { "importingYourAccount": {
"message": "Account impoteren..." "message": "Account impoteren"
}, },
"lastPassMFARequired": { "lastPassMFARequired": {
"message": "LastPass multifactor-authenticatie vereist" "message": "LastPass multifactor-authenticatie vereist"
@@ -3396,7 +3396,7 @@
"message": "Bestand op apparaat opgeslagen. Beheer vanaf de downloads op je apparaat." "message": "Bestand op apparaat opgeslagen. Beheer vanaf de downloads op je apparaat."
}, },
"importantNotice": { "importantNotice": {
"message": "Belangrijke mededeling" "message": "Belangrijke melding"
}, },
"setupTwoStepLogin": { "setupTwoStepLogin": {
"message": "Tweestapsaanmelding instellen" "message": "Tweestapsaanmelding instellen"

View File

@@ -3399,7 +3399,7 @@
"message": "Dôležité upozornenie" "message": "Dôležité upozornenie"
}, },
"setupTwoStepLogin": { "setupTwoStepLogin": {
"message": "Nastavenie dvojstupňového prihlásenia" "message": "Nastav dvojstupňové prihlásenie"
}, },
"newDeviceVerificationNoticeContentPage1": { "newDeviceVerificationNoticeContentPage1": {
"message": "Bitwarden vám od februára 2025 pošle na e-mail vášho účtu kód na overenie prihlásenia z nových zariadení." "message": "Bitwarden vám od februára 2025 pošle na e-mail vášho účtu kód na overenie prihlásenia z nových zariadení."

View File

@@ -208,19 +208,19 @@
"message": "Генерисан је нови SSH кључ" "message": "Генерисан је нови SSH кључ"
}, },
"sshKeyWrongPassword": { "sshKeyWrongPassword": {
"message": "The password you entered is incorrect." "message": "Лозинка коју сте унели није тачна."
}, },
"importSshKey": { "importSshKey": {
"message": "Import" "message": "Увоз"
}, },
"confirmSshKeyPassword": { "confirmSshKeyPassword": {
"message": "Confirm password" "message": "Потврда лозинке"
}, },
"enterSshKeyPasswordDesc": { "enterSshKeyPasswordDesc": {
"message": "Enter the password for the SSH key." "message": "Унети лозинку за SSH кључ."
}, },
"enterSshKeyPassword": { "enterSshKeyPassword": {
"message": "Enter password" "message": "Унесите лозинку"
}, },
"sshAgentUnlockRequired": { "sshAgentUnlockRequired": {
"message": "Откључајте свој сеф да бисте одобрили захтев за SSH кључ." "message": "Откључајте свој сеф да бисте одобрили захтев за SSH кључ."
@@ -920,10 +920,10 @@
"message": "УРЛ Сервера" "message": "УРЛ Сервера"
}, },
"authenticationTimeout": { "authenticationTimeout": {
"message": "Authentication timeout" "message": "Истекло је време аутентификације"
}, },
"authenticationSessionTimedOut": { "authenticationSessionTimedOut": {
"message": "The authentication session timed out. Please restart the login process." "message": "Истекло је време сесије за аутентификацију. Молим вас покрените процес пријаве поново."
}, },
"selfHostBaseUrl": { "selfHostBaseUrl": {
"message": "УРЛ сервера који се самостално хостује", "message": "УРЛ сервера који се самостално хостује",
@@ -1393,13 +1393,13 @@
"message": "Историја Лозинке" "message": "Историја Лозинке"
}, },
"generatorHistory": { "generatorHistory": {
"message": "Generator history" "message": "Генератор историје"
}, },
"clearGeneratorHistoryTitle": { "clearGeneratorHistoryTitle": {
"message": "Clear generator history" "message": "Испразнити генератор историје"
}, },
"cleargGeneratorHistoryDescription": { "cleargGeneratorHistoryDescription": {
"message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" "message": "Ако наставите, сви уноси ће бити трајно избрисани из генератора историје. Да ли сте сигурни да желите да наставите?"
}, },
"clear": { "clear": {
"message": "Очисти", "message": "Очисти",
@@ -1409,13 +1409,13 @@
"message": "Нема лозинки за приказивање." "message": "Нема лозинки за приказивање."
}, },
"clearHistory": { "clearHistory": {
"message": "Clear history" "message": "Обриши историју"
}, },
"nothingToShow": { "nothingToShow": {
"message": "Nothing to show" "message": "Ништа за приказ"
}, },
"nothingGeneratedRecently": { "nothingGeneratedRecently": {
"message": "You haven't generated anything recently" "message": "Недавно нисте ништа генерисали"
}, },
"undo": { "undo": {
"message": "Опозови" "message": "Опозови"
@@ -2481,7 +2481,7 @@
"message": "Генеришите имејл" "message": "Генеришите имејл"
}, },
"spinboxBoundariesHint": { "spinboxBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$.", "message": "Вредност мора бити између $MIN$ и $MAX$.",
"description": "Explains spin box minimum and maximum values to the user", "description": "Explains spin box minimum and maximum values to the user",
"placeholders": { "placeholders": {
"min": { "min": {
@@ -2495,7 +2495,7 @@
} }
}, },
"passwordLengthRecommendationHint": { "passwordLengthRecommendationHint": {
"message": " Use $RECOMMENDED$ characters or more to generate a strong password.", "message": " Употребити $RECOMMENDED$ знакова или више да бисте генерисали јаку лозинку.",
"description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": { "placeholders": {
"recommended": { "recommended": {
@@ -2505,7 +2505,7 @@
} }
}, },
"passphraseNumWordsRecommendationHint": { "passphraseNumWordsRecommendationHint": {
"message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", "message": " Употребити $RECOMMENDED$ речи или више да бисте генерисали јаку приступну фразу.",
"description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": { "placeholders": {
"recommended": { "recommended": {
@@ -2726,13 +2726,13 @@
"message": "Обавештење је послато на ваш уређај." "message": "Обавештење је послато на ваш уређај."
}, },
"aNotificationWasSentToYourDevice": { "aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device" "message": "Обавештење је послато на ваш уређај"
}, },
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" "message": "Уверите се да је ваш налог откључан и да се фраза отиска подудара на другом уређају"
}, },
"needAnotherOptionV1": { "needAnotherOptionV1": {
"message": "Need another option?" "message": "Треба Вам друга опција?"
}, },
"fingerprintMatchInfo": { "fingerprintMatchInfo": {
"message": "Уверите се да је ваш сеф откључан и да се фраза отиска прста подудара на другом уређају." "message": "Уверите се да је ваш сеф откључан и да се фраза отиска прста подудара на другом уређају."
@@ -2741,13 +2741,13 @@
"message": "Сигурносна фраза сефа" "message": "Сигурносна фраза сефа"
}, },
"youWillBeNotifiedOnceTheRequestIsApproved": { "youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved" "message": "Бићете обавештени када захтев буде одобрен"
}, },
"needAnotherOption": { "needAnotherOption": {
"message": "Пријава помоћу уређаја мора бити подешена у подешавањима Bitwarden апликације. Потребна је друга опција?" "message": "Пријава помоћу уређаја мора бити подешена у подешавањима Bitwarden апликације. Потребна је друга опција?"
}, },
"viewAllLogInOptions": { "viewAllLogInOptions": {
"message": "View all log in options" "message": "Погледајте сав извештај у опције"
}, },
"viewAllLoginOptions": { "viewAllLoginOptions": {
"message": "Погредајте све опције пријављивања" "message": "Погредајте све опције пријављивања"
@@ -2869,7 +2869,7 @@
"message": "Проверите познате упада података за ову лозинку" "message": "Проверите познате упада података за ову лозинку"
}, },
"loggedInExclamation": { "loggedInExclamation": {
"message": "Logged in!" "message": "Пријављено!"
}, },
"important": { "important": {
"message": "Важно:" "message": "Важно:"
@@ -2902,16 +2902,16 @@
"message": "Препоручено ажурирање поставки" "message": "Препоручено ажурирање поставки"
}, },
"rememberThisDeviceToMakeFutureLoginsSeamless": { "rememberThisDeviceToMakeFutureLoginsSeamless": {
"message": "Remember this device to make future logins seamless" "message": "Запамтити овај уређај да би будуће пријаве биле беспрекорне"
}, },
"deviceApprovalRequired": { "deviceApprovalRequired": {
"message": "Потребно је одобрење уређаја. Изаберите опцију одобрења испод:" "message": "Потребно је одобрење уређаја. Изаберите опцију одобрења испод:"
}, },
"deviceApprovalRequiredV2": { "deviceApprovalRequiredV2": {
"message": "Device approval required" "message": "Потребно је одобрење уређаја"
}, },
"selectAnApprovalOptionBelow": { "selectAnApprovalOptionBelow": {
"message": "Select an approval option below" "message": "Изаберите опцију одобрења у наставку"
}, },
"rememberThisDevice": { "rememberThisDevice": {
"message": "Запамти овај уређај" "message": "Запамти овај уређај"
@@ -2966,7 +2966,7 @@
"message": "Недостаје имејл корисника" "message": "Недостаје имејл корисника"
}, },
"activeUserEmailNotFoundLoggingYouOut": { "activeUserEmailNotFoundLoggingYouOut": {
"message": "Active user email not found. Logging you out." "message": "Имејл активног корисника није пронађен. Одјављивање."
}, },
"deviceTrusted": { "deviceTrusted": {
"message": "Уређај поуздан" "message": "Уређај поуздан"

View File

@@ -3363,10 +3363,10 @@
"message": "SSO girişi için açık port bulunamadı." "message": "SSO girişi için açık port bulunamadı."
}, },
"authorize": { "authorize": {
"message": "Authorize" "message": "Yetkilendir"
}, },
"deny": { "deny": {
"message": "Deny" "message": "Reddet"
}, },
"sshkeyApprovalTitle": { "sshkeyApprovalTitle": {
"message": "Confirm SSH key usage" "message": "Confirm SSH key usage"

View File

@@ -742,7 +742,7 @@
"message": "必须填写确认主密码。" "message": "必须填写确认主密码。"
}, },
"masterPasswordMinlength": { "masterPasswordMinlength": {
"message": "主密码必须至少 $VALUE$ 个字符长度。", "message": "主密码长度必须至少 $VALUE$ 个字符。",
"description": "The Master Password must be at least a specific number of characters long.", "description": "The Master Password must be at least a specific number of characters long.",
"placeholders": { "placeholders": {
"value": { "value": {
@@ -1044,7 +1044,7 @@
"message": "前往网页 App 吗?" "message": "前往网页 App 吗?"
}, },
"changeMasterPasswordOnWebConfirmation": { "changeMasterPasswordOnWebConfirmation": {
"message": "您可以在 Bitwarden 网页应用上更改您的主密码。" "message": "您可以在 Bitwarden 网页 App 上更改您的主密码。"
}, },
"fingerprintPhrase": { "fingerprintPhrase": {
"message": "指纹短语", "message": "指纹短语",
@@ -1058,7 +1058,7 @@
"message": "转到网页版密码库" "message": "转到网页版密码库"
}, },
"getMobileApp": { "getMobileApp": {
"message": "获取移动应用程序" "message": "获取移动 App"
}, },
"getBrowserExtension": { "getBrowserExtension": {
"message": "获取浏览器扩展" "message": "获取浏览器扩展"
@@ -1247,13 +1247,13 @@
"message": "语言" "message": "语言"
}, },
"languageDesc": { "languageDesc": {
"message": "更改应用程序所使用的语言。重新启动后生效。" "message": "更改应用程序所使用的语言。重后生效。"
}, },
"theme": { "theme": {
"message": "主题" "message": "主题"
}, },
"themeDesc": { "themeDesc": {
"message": "更改应用程序的颜色主题。" "message": "更改应用程序的颜色主题。"
}, },
"dark": { "dark": {
"message": "深色", "message": "深色",
@@ -1665,7 +1665,7 @@
"message": "确认密码库导出" "message": "确认密码库导出"
}, },
"exportWarningDesc": { "exportWarningDesc": {
"message": "导出的密码库数据包含未加密格式。您不应该通过不安全的渠道(例如电子邮件)来存储或发送导出文件。用完后请立即将其删除。" "message": "导出包含未加密格式的密码库数据。您不应该通过不安全的渠道(例如电子邮件)来存储或发送导出文件。使用完后请立即将其删除。"
}, },
"encExportKeyWarningDesc": { "encExportKeyWarningDesc": {
"message": "此导出将使用您账户的加密密钥来加密您的数据。如果您曾经轮换过账户的加密密钥,您应将其重新导出,否则您将无法解密导出的文件。" "message": "此导出将使用您账户的加密密钥来加密您的数据。如果您曾经轮换过账户的加密密钥,您应将其重新导出,否则您将无法解密导出的文件。"
@@ -1711,7 +1711,7 @@
"message": "使用 PIN 码解锁" "message": "使用 PIN 码解锁"
}, },
"setYourPinCode": { "setYourPinCode": {
"message": "设定您用来解锁 Bitwarden 的 PIN 码。您的 PIN 设置将在您完全注销应用程序时被重置。" "message": "设置用于解锁 Bitwarden 的 PIN 码。您的 PIN 设置将在您完全注销应用程序时被重置。"
}, },
"pinRequired": { "pinRequired": {
"message": "需要 PIN 码。" "message": "需要 PIN 码。"
@@ -1753,7 +1753,7 @@
"message": "应用程序启动时要求使用触控 ID" "message": "应用程序启动时要求使用触控 ID"
}, },
"requirePasswordOnStart": { "requirePasswordOnStart": {
"message": "应用程序启动时要求输入密码或 PIN 码" "message": "App 启动时要求输入密码或 PIN 码"
}, },
"recommendedForSecurity": { "recommendedForSecurity": {
"message": "安全起见,推荐设置。" "message": "安全起见,推荐设置。"
@@ -2402,7 +2402,7 @@
"message": "偏好设置" "message": "偏好设置"
}, },
"appPreferences": { "appPreferences": {
"message": "应用设置(所有账户)" "message": "应用程序设置(所有账户)"
}, },
"accountSwitcherLimitReached": { "accountSwitcherLimitReached": {
"message": "已达到账户上限。请注销一个账户后再添加其他账户。" "message": "已达到账户上限。请注销一个账户后再添加其他账户。"
@@ -3369,7 +3369,7 @@
"message": "拒绝" "message": "拒绝"
}, },
"sshkeyApprovalTitle": { "sshkeyApprovalTitle": {
"message": "确认 SSH 密钥的使用方式" "message": "确认 SSH 密钥的使用"
}, },
"sshkeyApprovalMessageInfix": { "sshkeyApprovalMessageInfix": {
"message": "正在请求访问" "message": "正在请求访问"
@@ -3402,7 +3402,7 @@
"message": "设置两步登录" "message": "设置两步登录"
}, },
"newDeviceVerificationNoticeContentPage1": { "newDeviceVerificationNoticeContentPage1": {
"message": "从 2025 年 02 月开始Bitwarden 将向您的账户电子邮箱发送一个代码,以验证来自新设备的登录。" "message": "从 2025 年 02 月起,当有来自新设备的登录时Bitwarden 将向您的账户电子邮箱发送验证码。"
}, },
"newDeviceVerificationNoticeContentPage2": { "newDeviceVerificationNoticeContentPage2": {
"message": "您可以设置两步登录作为保护账户的替代方法,或将您的电子邮箱更改为您可以访问的电子邮箱。" "message": "您可以设置两步登录作为保护账户的替代方法,或将您的电子邮箱更改为您可以访问的电子邮箱。"
@@ -3411,7 +3411,7 @@
"message": "稍后提醒我" "message": "稍后提醒我"
}, },
"newDeviceVerificationNoticePageOneFormContent": { "newDeviceVerificationNoticePageOneFormContent": {
"message": "您能可靠地访问您的电子邮箱 $EMAIL$ 吗?", "message": "您能可正常访问您的电子邮箱 $EMAIL$ 吗?",
"placeholders": { "placeholders": {
"email": { "email": {
"content": "$1", "content": "$1",
@@ -3423,7 +3423,7 @@
"message": "不,我不能" "message": "不,我不能"
}, },
"newDeviceVerificationNoticePageOneEmailAccessYes": { "newDeviceVerificationNoticePageOneEmailAccessYes": {
"message": "是的,我可以可靠地访问我的电子邮箱" "message": "是的,我可以正常访问我的电子邮箱"
}, },
"turnOnTwoStepLogin": { "turnOnTwoStepLogin": {
"message": "开启两步登录" "message": "开启两步登录"

View File

@@ -0,0 +1,38 @@
<bit-dialog #dialog dialogSize="large" background="alt">
<span bitDialogTitle>{{ "generator" | i18n }}</span>
<ng-container bitDialogContent>
<vault-cipher-form-generator
[type]="data.type"
(valueGenerated)="onCredentialGenerated($event)"
/>
<bit-item>
<button
type="button"
bitLink
linkType="primary"
bit-item-content
aria-haspopup="true"
(click)="openHistoryDialog()"
>
{{ "generatorHistory" | i18n }}
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</button>
</bit-item>
</ng-container>
<ng-container bitDialogFooter>
<div class="modal-footer">
<button
type="button"
class="primary"
(click)="applyCredentials()"
appA11yTitle="{{ 'select' | i18n }}"
bitDialogClose
>
<i class="bwi bwi-lg bwi-fw bwi-check" aria-hidden="true"></i>
</button>
<button type="button" data-dismiss="modal" (click)="clearCredentials()" bitDialogClose>
{{ "cancel" | i18n }}
</button>
</div>
</ng-container>
</bit-dialog>

View File

@@ -0,0 +1,69 @@
import { DIALOG_DATA } from "@angular/cdk/dialog";
import { CommonModule } from "@angular/common";
import { Component, Inject } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
ButtonModule,
DialogModule,
DialogService,
ItemModule,
LinkModule,
} from "@bitwarden/components";
import {
CredentialGeneratorHistoryDialogComponent,
GeneratorModule,
} from "@bitwarden/generator-components";
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
type CredentialGeneratorParams = {
onCredentialGenerated: (value?: string) => void;
type: "password" | "username";
};
@Component({
standalone: true,
selector: "credential-generator-dialog",
templateUrl: "credential-generator-dialog.component.html",
imports: [
CipherFormGeneratorComponent,
CommonModule,
DialogModule,
ButtonModule,
JslibModule,
GeneratorModule,
ItemModule,
LinkModule,
],
})
export class CredentialGeneratorDialogComponent {
credentialValue?: string;
constructor(
@Inject(DIALOG_DATA) protected data: CredentialGeneratorParams,
private dialogService: DialogService,
) {}
applyCredentials = () => {
this.data.onCredentialGenerated(this.credentialValue);
};
clearCredentials = () => {
this.data.onCredentialGenerated();
};
onCredentialGenerated = (value: string) => {
this.credentialValue = value;
};
openHistoryDialog = () => {
// open history dialog
this.dialogService.open(CredentialGeneratorHistoryDialogComponent);
};
static open = (dialogService: DialogService, data: CredentialGeneratorParams) => {
dialogService.open(CredentialGeneratorDialogComponent, {
data,
});
};
}

View File

@@ -20,7 +20,9 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -40,6 +42,7 @@ import { invokeMenu, RendererMenuItem } from "../../../utils";
import { AddEditComponent } from "./add-edit.component"; import { AddEditComponent } from "./add-edit.component";
import { AttachmentsComponent } from "./attachments.component"; import { AttachmentsComponent } from "./attachments.component";
import { CollectionsComponent } from "./collections.component"; import { CollectionsComponent } from "./collections.component";
import { CredentialGeneratorDialogComponent } from "./credential-generator-dialog.component";
import { FolderAddEditComponent } from "./folder-add-edit.component"; import { FolderAddEditComponent } from "./folder-add-edit.component";
import { PasswordHistoryComponent } from "./password-history.component"; import { PasswordHistoryComponent } from "./password-history.component";
import { ShareComponent } from "./share.component"; import { ShareComponent } from "./share.component";
@@ -107,6 +110,7 @@ export class VaultComponent implements OnInit, OnDestroy {
private apiService: ApiService, private apiService: ApiService,
private dialogService: DialogService, private dialogService: DialogService,
private billingAccountProfileStateService: BillingAccountProfileStateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
private configService: ConfigService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
@@ -622,11 +626,29 @@ export class VaultComponent implements OnInit, OnDestroy {
} }
async openGenerator(comingFromAddEdit: boolean, passwordType = true) { async openGenerator(comingFromAddEdit: boolean, passwordType = true) {
// FIXME: Will need to be extended to use the cipher-form-generator component introduced with https://github.com/bitwarden/clients/pull/11350 const isGeneratorSwapEnabled = await this.configService.getFeatureFlag(
if (this.modal != null) { FeatureFlag.GeneratorToolsModernization,
this.modal.close(); );
if (isGeneratorSwapEnabled) {
CredentialGeneratorDialogComponent.open(this.dialogService, {
onCredentialGenerated: (value?: string) => {
if (this.addEditComponent != null) {
this.addEditComponent.markPasswordAsDirty();
if (passwordType) {
this.addEditComponent.cipher.login.password = value ?? "";
} else {
this.addEditComponent.cipher.login.username = value ?? "";
}
}
},
type: passwordType ? "password" : "username",
});
return;
} }
// TODO: Legacy code below, remove once the new generator is fully implemented
// https://bitwarden.atlassian.net/browse/PM-7121
const cipher = this.addEditComponent?.cipher; const cipher = this.addEditComponent?.cipher;
const loginType = cipher != null && cipher.type === CipherType.Login && cipher.login != null; const loginType = cipher != null && cipher.type === CipherType.Login && cipher.login != null;

View File

@@ -9,9 +9,11 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { IntegrationType } from "@bitwarden/common/enums"; import { IntegrationType } from "@bitwarden/common/enums";
import { HeaderModule } from "../../../layouts/header/header.module"; import { HeaderModule } from "../../../layouts/header/header.module";
import { FilterIntegrationsPipe, IntegrationGridComponent, Integration } from "../../../shared/";
import { SharedModule } from "../../../shared/shared.module"; import { SharedModule } from "../../../shared/shared.module";
import { SharedOrganizationModule } from "../shared"; import { SharedOrganizationModule } from "../shared";
import { IntegrationGridComponent } from "../shared/components/integrations/integration-grid/integration-grid.component";
import { FilterIntegrationsPipe } from "../shared/components/integrations/integrations.pipe";
import { Integration } from "../shared/components/integrations/models";
@Component({ @Component({
selector: "ac-integrations", selector: "ac-integrations",

View File

@@ -131,7 +131,9 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe
protected getUserName(r: EventResponse, userId: string) { protected getUserName(r: EventResponse, userId: string) {
if (r.installationId != null) { if (r.installationId != null) {
return `Installation: ${r.installationId}`; return {
name: `Installation: ${r.installationId}`,
};
} }
if (userId != null) { if (userId != null) {

View File

@@ -0,0 +1,4 @@
export * from "./integrations.pipe";
export * from "./integration-card/integration-card.component";
export * from "./integration-grid/integration-grid.component";
export * from "./models";

View File

@@ -15,7 +15,7 @@ import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-t
import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeType } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { SharedModule } from "../../../shared.module"; import { SharedModule } from "../../../../../../shared/shared.module";
@Component({ @Component({
selector: "app-integration-card", selector: "app-integration-card",

View File

@@ -1,13 +1,12 @@
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; import { importProvidersFrom } from "@angular/core";
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
import { of } from "rxjs"; import { of } from "rxjs";
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ThemeTypes } from "@bitwarden/common/platform/enums"; import { ThemeTypes } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { I18nMockService } from "@bitwarden/components";
import { SharedModule } from "../../../shared.module"; import { PreloadedEnglishI18nModule } from "../../../../../../core/tests";
import { IntegrationCardComponent } from "./integration-card.component"; import { IntegrationCardComponent } from "./integration-card.component";
@@ -17,15 +16,11 @@ export default {
title: "Web/Integration Layout/Integration Card", title: "Web/Integration Layout/Integration Card",
component: IntegrationCardComponent, component: IntegrationCardComponent,
decorators: [ decorators: [
applicationConfig({
providers: [importProvidersFrom(PreloadedEnglishI18nModule)],
}),
moduleMetadata({ moduleMetadata({
imports: [SharedModule],
providers: [ providers: [
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({});
},
},
{ {
provide: ThemeStateService, provide: ThemeStateService,
useClass: MockThemeService, useClass: MockThemeService,

View File

@@ -4,7 +4,7 @@ import { Component, Input } from "@angular/core";
import { IntegrationType } from "@bitwarden/common/enums"; import { IntegrationType } from "@bitwarden/common/enums";
import { SharedModule } from "../../../shared.module"; import { SharedModule } from "../../../../../../shared/shared.module";
import { IntegrationCardComponent } from "../integration-card/integration-card.component"; import { IntegrationCardComponent } from "../integration-card/integration-card.component";
import { Integration } from "../models"; import { Integration } from "../models";

View File

@@ -1,14 +1,13 @@
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; import { importProvidersFrom } from "@angular/core";
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
import { of } from "rxjs"; import { of } from "rxjs";
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
import { IntegrationType } from "@bitwarden/common/enums"; import { IntegrationType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ThemeTypes } from "@bitwarden/common/platform/enums"; import { ThemeTypes } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { I18nMockService } from "@bitwarden/components";
import { SharedModule } from "../../../shared.module"; import { PreloadedEnglishI18nModule } from "../../../../../../core/tests";
import { IntegrationCardComponent } from "../integration-card/integration-card.component"; import { IntegrationCardComponent } from "../integration-card/integration-card.component";
import { IntegrationGridComponent } from "../integration-grid/integration-grid.component"; import { IntegrationGridComponent } from "../integration-grid/integration-grid.component";
@@ -18,18 +17,12 @@ export default {
title: "Web/Integration Layout/Integration Grid", title: "Web/Integration Layout/Integration Grid",
component: IntegrationGridComponent, component: IntegrationGridComponent,
decorators: [ decorators: [
applicationConfig({
providers: [importProvidersFrom(PreloadedEnglishI18nModule)],
}),
moduleMetadata({ moduleMetadata({
imports: [IntegrationCardComponent, SharedModule], imports: [IntegrationCardComponent],
providers: [ providers: [
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
integrationCardAriaLabel: "Go to integration",
integrationCardTooltip: "Go to integration",
});
},
},
{ {
provide: ThemeStateService, provide: ThemeStateService,
useClass: MockThemeService, useClass: MockThemeService,

View File

@@ -3,7 +3,7 @@ import { LayoutModule } from "@angular/cdk/layout";
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms"; import { FormsModule } from "@angular/forms";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { InfiniteScrollModule } from "ngx-infinite-scroll"; import { InfiniteScrollDirective } from "ngx-infinite-scroll";
import { AppComponent } from "./app.component"; import { AppComponent } from "./app.component";
import { CoreModule } from "./core"; import { CoreModule } from "./core";
@@ -23,7 +23,7 @@ import { WildcardRoutingModule } from "./wildcard-routing.module";
BrowserAnimationsModule, BrowserAnimationsModule,
FormsModule, FormsModule,
CoreModule, CoreModule,
InfiniteScrollModule, InfiniteScrollDirective,
DragDropModule, DragDropModule,
LayoutModule, LayoutModule,
OssRoutingModule, OssRoutingModule,

View File

@@ -1,4 +0,0 @@
export * from "./integrations/integration-card/integration-card.component";
export * from "./integrations/integration-grid/integration-grid.component";
export * from "./integrations/integrations.pipe";
export * from "./integrations/models";

View File

@@ -1,3 +1,2 @@
export * from "./shared.module"; export * from "./shared.module";
export * from "./loose-components.module"; export * from "./loose-components.module";
export * from "./components/index";

View File

@@ -3,7 +3,7 @@ import { CommonModule, DatePipe } from "@angular/common";
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { RouterModule } from "@angular/router"; import { RouterModule } from "@angular/router";
import { InfiniteScrollModule } from "ngx-infinite-scroll"; import { InfiniteScrollDirective } from "ngx-infinite-scroll";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { import {
@@ -49,7 +49,7 @@ import "./locales";
DragDropModule, DragDropModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
InfiniteScrollModule, InfiniteScrollDirective,
RouterModule, RouterModule,
JslibModule, JslibModule,
@@ -86,7 +86,7 @@ import "./locales";
DragDropModule, DragDropModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
InfiniteScrollModule, InfiniteScrollDirective,
RouterModule, RouterModule,
JslibModule, JslibModule,

View File

@@ -9,9 +9,11 @@ import { map } from "rxjs/operators";
import { CollectionView } from "@bitwarden/admin-console/common"; import { CollectionView } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@@ -237,6 +239,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
private premiumUpgradeService: PremiumUpgradePromptService, private premiumUpgradeService: PremiumUpgradePromptService,
private cipherAuthorizationService: CipherAuthorizationService, private cipherAuthorizationService: CipherAuthorizationService,
private apiService: ApiService, private apiService: ApiService,
private eventCollectionService: EventCollectionService,
) { ) {
this.updateTitle(); this.updateTitle();
} }
@@ -257,6 +260,13 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
[this.params.activeCollectionId], [this.params.activeCollectionId],
this.params.isAdminConsoleAction, this.params.isAdminConsoleAction,
); );
await this.eventCollectionService.collect(
EventType.Cipher_ClientViewed,
this.cipher.id,
false,
this.cipher.organizationId,
);
} }
this.performingInitialLoad = false; this.performingInitialLoad = false;

View File

@@ -9271,6 +9271,18 @@
"updatedTaxInformation": { "updatedTaxInformation": {
"message": "Updated tax information" "message": "Updated tax information"
}, },
"billingInvalidTaxIdError": {
"message": "Invalid tax ID, if you believe this is an error please contact support."
},
"billingTaxIdTypeInferenceError": {
"message": "We were unable to validate your tax ID, if you believe this is an error please contact support."
},
"billingPreviewInvalidTaxIdError": {
"message": "Invalid tax ID, if you believe this is an error please contact support."
},
"billingPreviewInvoiceError": {
"message": "An error occurred while previewing the invoice. Please try again later."
},
"unverified": { "unverified": {
"message": "Unverified" "message": "Unverified"
}, },
@@ -10007,5 +10019,48 @@
}, },
"organizationNameMaxLength": { "organizationNameMaxLength": {
"message": "Organization name cannot exceed 50 characters." "message": "Organization name cannot exceed 50 characters."
},
"resellerRenewalWarning": {
"message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"renewal_date": {
"content": "$2",
"example": "01/01/2024"
}
}
},
"resellerOpenInvoiceWarning": {
"message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"issued_date": {
"content": "$2",
"example": "01/01/2024"
},
"due_date": {
"content": "$3",
"example": "01/15/2024"
}
}
},
"resellerPastDueWarning": {
"message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"grace_period_end": {
"content": "$2",
"example": "02/14/2024"
}
}
} }
} }

View File

@@ -9271,6 +9271,18 @@
"updatedTaxInformation": { "updatedTaxInformation": {
"message": "Updated tax information" "message": "Updated tax information"
}, },
"billingInvalidTaxIdError": {
"message": "Invalid tax ID, if you believe this is an error please contact support."
},
"billingTaxIdTypeInferenceError": {
"message": "We were unable to validate your tax ID, if you believe this is an error please contact support."
},
"billingPreviewInvalidTaxIdError": {
"message": "Invalid tax ID, if you believe this is an error please contact support."
},
"billingPreviewInvoiceError": {
"message": "An error occurred while previewing the invoice. Please try again later."
},
"unverified": { "unverified": {
"message": "Unverified" "message": "Unverified"
}, },
@@ -10007,5 +10019,48 @@
}, },
"organizationNameMaxLength": { "organizationNameMaxLength": {
"message": "Organization name cannot exceed 50 characters." "message": "Organization name cannot exceed 50 characters."
},
"resellerRenewalWarning": {
"message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"renewal_date": {
"content": "$2",
"example": "01/01/2024"
}
}
},
"resellerOpenInvoiceWarning": {
"message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"issued_date": {
"content": "$2",
"example": "01/01/2024"
},
"due_date": {
"content": "$3",
"example": "01/15/2024"
}
}
},
"resellerPastDueWarning": {
"message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"grace_period_end": {
"content": "$2",
"example": "02/14/2024"
}
}
} }
} }

View File

@@ -9271,6 +9271,18 @@
"updatedTaxInformation": { "updatedTaxInformation": {
"message": "Güncəlllənən vergi məlumatı" "message": "Güncəlllənən vergi məlumatı"
}, },
"billingInvalidTaxIdError": {
"message": "Yararsız vergi kimliyi, bunun xəta olduğunu düşünürsünüzsə, dəstək komandası ilə əlaqə saxlayın."
},
"billingTaxIdTypeInferenceError": {
"message": "Vergi kimliyi nömrənizi doğrulaya bilmədik, bunun xəta olduğunu düşünürsünüzsə, dəstək komandası ilə əlaqə saxlayın."
},
"billingPreviewInvalidTaxIdError": {
"message": "Yararsız vergi kimliyi, bunun xəta olduğunu düşünürsünüzsə, dəstək komandası ilə əlaqə saxlayın."
},
"billingPreviewInvoiceError": {
"message": "Faktura önizləməsi zamanı bir xəta baş verdi. Lütfən daha sonra yenidən sınayın."
},
"unverified": { "unverified": {
"message": "Doğrulanmayıb" "message": "Doğrulanmayıb"
}, },
@@ -10007,5 +10019,48 @@
}, },
"organizationNameMaxLength": { "organizationNameMaxLength": {
"message": "Təşkilat adı 50 xarakterdən çox ola bilməz." "message": "Təşkilat adı 50 xarakterdən çox ola bilməz."
},
"resellerRenewalWarning": {
"message": "Abunəliyiniz tezliklə yenilənəcək. Kəsintisiz xidməti təmin etmək və yeniləməni $RENEWAL_DATE$ tarixindən əvvəl təsdiqləmək üçün $RESELLER$ ilə əlaqə saxlayın.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"renewal_date": {
"content": "$2",
"example": "01/01/2024"
}
}
},
"resellerOpenInvoiceWarning": {
"message": "Abunəliyinizə aid faktura $ISSUED_DATE$ tarixində təqdim edildi. Kəsintisiz xidməti təmin etmək və yeniləməni $DUE_DATE$ tarixindən əvvəl təsdiqləmək üçün $RESELLER$ ilə əlaqə saxlayın.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"issued_date": {
"content": "$2",
"example": "01/01/2024"
},
"due_date": {
"content": "$3",
"example": "01/15/2024"
}
}
},
"resellerPastDueWarning": {
"message": "Abunəliyinizə aid faktura üzrə ödəniş edilmədi. Kəsintisiz xidməti təmin etmək və yeniləməni $GRACE_PERIOD_END$ tarixindən əvvəl təsdiqləmək üçün $RESELLER$ ilə əlaqə saxlayın.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"grace_period_end": {
"content": "$2",
"example": "02/14/2024"
}
}
} }
} }

View File

@@ -9271,6 +9271,18 @@
"updatedTaxInformation": { "updatedTaxInformation": {
"message": "Updated tax information" "message": "Updated tax information"
}, },
"billingInvalidTaxIdError": {
"message": "Invalid tax ID, if you believe this is an error please contact support."
},
"billingTaxIdTypeInferenceError": {
"message": "We were unable to validate your tax ID, if you believe this is an error please contact support."
},
"billingPreviewInvalidTaxIdError": {
"message": "Invalid tax ID, if you believe this is an error please contact support."
},
"billingPreviewInvoiceError": {
"message": "An error occurred while previewing the invoice. Please try again later."
},
"unverified": { "unverified": {
"message": "Unverified" "message": "Unverified"
}, },
@@ -10007,5 +10019,48 @@
}, },
"organizationNameMaxLength": { "organizationNameMaxLength": {
"message": "Organization name cannot exceed 50 characters." "message": "Organization name cannot exceed 50 characters."
},
"resellerRenewalWarning": {
"message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"renewal_date": {
"content": "$2",
"example": "01/01/2024"
}
}
},
"resellerOpenInvoiceWarning": {
"message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"issued_date": {
"content": "$2",
"example": "01/01/2024"
},
"due_date": {
"content": "$3",
"example": "01/15/2024"
}
}
},
"resellerPastDueWarning": {
"message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"grace_period_end": {
"content": "$2",
"example": "02/14/2024"
}
}
} }
} }

View File

@@ -9271,6 +9271,18 @@
"updatedTaxInformation": { "updatedTaxInformation": {
"message": "Обновена данъчна информация" "message": "Обновена данъчна информация"
}, },
"billingInvalidTaxIdError": {
"message": "Неправилен данъчен идентификатор. Ако смятате, че това е грешка, свържете се с поддръжката."
},
"billingTaxIdTypeInferenceError": {
"message": "Не успяхме да потвърдим Вашия данъчен идентификатор. Ако смятате, че това е грешка, свържете се с поддръжката."
},
"billingPreviewInvalidTaxIdError": {
"message": "Неправилен данъчен идентификатор. Ако смятате, че това е грешка, свържете се с поддръжката."
},
"billingPreviewInvoiceError": {
"message": "Възникна грешка при преглеждането на фактурата. Опитайте отново по-късно."
},
"unverified": { "unverified": {
"message": "Непотвърден" "message": "Непотвърден"
}, },
@@ -10007,5 +10019,48 @@
}, },
"organizationNameMaxLength": { "organizationNameMaxLength": {
"message": "Името на организацията не може да бъде по-дълго от 50 знака." "message": "Името на организацията не може да бъде по-дълго от 50 знака."
},
"resellerRenewalWarning": {
"message": "Вашият абонамент ще бъде подновен скоро. За да подсигурите, че услугата няма да има прекъсвания, свържете се с $RESELLER$ и потвърдете подновяването преди $RENEWAL_DATE$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"renewal_date": {
"content": "$2",
"example": "01/01/2024"
}
}
},
"resellerOpenInvoiceWarning": {
"message": "Фактура за абонамента Ви беше издадена на $ISSUED_DATE$. За да подсигурите, че услугата няма да има прекъсвания, свържете се с $RESELLER$ и потвърдете подновяването преди $DUE_DATE$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"issued_date": {
"content": "$2",
"example": "01/01/2024"
},
"due_date": {
"content": "$3",
"example": "01/15/2024"
}
}
},
"resellerPastDueWarning": {
"message": "Фактурата за абонамента Ви не е била платена. За да подсигурите, че услугата няма да има прекъсвания, свържете се с $RESELLER$ и потвърдете подновяването преди $GRACE_PERIOD_END$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"grace_period_end": {
"content": "$2",
"example": "02/14/2024"
}
}
} }
} }

View File

@@ -9271,6 +9271,18 @@
"updatedTaxInformation": { "updatedTaxInformation": {
"message": "Updated tax information" "message": "Updated tax information"
}, },
"billingInvalidTaxIdError": {
"message": "Invalid tax ID, if you believe this is an error please contact support."
},
"billingTaxIdTypeInferenceError": {
"message": "We were unable to validate your tax ID, if you believe this is an error please contact support."
},
"billingPreviewInvalidTaxIdError": {
"message": "Invalid tax ID, if you believe this is an error please contact support."
},
"billingPreviewInvoiceError": {
"message": "An error occurred while previewing the invoice. Please try again later."
},
"unverified": { "unverified": {
"message": "Unverified" "message": "Unverified"
}, },
@@ -10007,5 +10019,48 @@
}, },
"organizationNameMaxLength": { "organizationNameMaxLength": {
"message": "Organization name cannot exceed 50 characters." "message": "Organization name cannot exceed 50 characters."
},
"resellerRenewalWarning": {
"message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"renewal_date": {
"content": "$2",
"example": "01/01/2024"
}
}
},
"resellerOpenInvoiceWarning": {
"message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"issued_date": {
"content": "$2",
"example": "01/01/2024"
},
"due_date": {
"content": "$3",
"example": "01/15/2024"
}
}
},
"resellerPastDueWarning": {
"message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"grace_period_end": {
"content": "$2",
"example": "02/14/2024"
}
}
} }
} }

View File

@@ -9271,6 +9271,18 @@
"updatedTaxInformation": { "updatedTaxInformation": {
"message": "Updated tax information" "message": "Updated tax information"
}, },
"billingInvalidTaxIdError": {
"message": "Invalid tax ID, if you believe this is an error please contact support."
},
"billingTaxIdTypeInferenceError": {
"message": "We were unable to validate your tax ID, if you believe this is an error please contact support."
},
"billingPreviewInvalidTaxIdError": {
"message": "Invalid tax ID, if you believe this is an error please contact support."
},
"billingPreviewInvoiceError": {
"message": "An error occurred while previewing the invoice. Please try again later."
},
"unverified": { "unverified": {
"message": "Unverified" "message": "Unverified"
}, },
@@ -10007,5 +10019,48 @@
}, },
"organizationNameMaxLength": { "organizationNameMaxLength": {
"message": "Organization name cannot exceed 50 characters." "message": "Organization name cannot exceed 50 characters."
},
"resellerRenewalWarning": {
"message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"renewal_date": {
"content": "$2",
"example": "01/01/2024"
}
}
},
"resellerOpenInvoiceWarning": {
"message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"issued_date": {
"content": "$2",
"example": "01/01/2024"
},
"due_date": {
"content": "$3",
"example": "01/15/2024"
}
}
},
"resellerPastDueWarning": {
"message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"grace_period_end": {
"content": "$2",
"example": "02/14/2024"
}
}
} }
} }

View File

@@ -9271,6 +9271,18 @@
"updatedTaxInformation": { "updatedTaxInformation": {
"message": "Updated tax information" "message": "Updated tax information"
}, },
"billingInvalidTaxIdError": {
"message": "Invalid tax ID, if you believe this is an error please contact support."
},
"billingTaxIdTypeInferenceError": {
"message": "We were unable to validate your tax ID, if you believe this is an error please contact support."
},
"billingPreviewInvalidTaxIdError": {
"message": "Invalid tax ID, if you believe this is an error please contact support."
},
"billingPreviewInvoiceError": {
"message": "An error occurred while previewing the invoice. Please try again later."
},
"unverified": { "unverified": {
"message": "Unverified" "message": "Unverified"
}, },
@@ -10007,5 +10019,48 @@
}, },
"organizationNameMaxLength": { "organizationNameMaxLength": {
"message": "Organization name cannot exceed 50 characters." "message": "Organization name cannot exceed 50 characters."
},
"resellerRenewalWarning": {
"message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"renewal_date": {
"content": "$2",
"example": "01/01/2024"
}
}
},
"resellerOpenInvoiceWarning": {
"message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"issued_date": {
"content": "$2",
"example": "01/01/2024"
},
"due_date": {
"content": "$3",
"example": "01/15/2024"
}
}
},
"resellerPastDueWarning": {
"message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"grace_period_end": {
"content": "$2",
"example": "02/14/2024"
}
}
} }
} }

View File

@@ -9271,6 +9271,18 @@
"updatedTaxInformation": { "updatedTaxInformation": {
"message": "Aktualizované daňové údaje" "message": "Aktualizované daňové údaje"
}, },
"billingInvalidTaxIdError": {
"message": "Neplatné DIČ. Pokud se domníváte, že se jedná o chybu, kontaktujte podporu."
},
"billingTaxIdTypeInferenceError": {
"message": "Nebyli jsme schopni ověřit Vaše DIČ. Pokud se domníváte, že se jedná o chybu, kontaktujte podporu."
},
"billingPreviewInvalidTaxIdError": {
"message": "Neplatné DIČ. Pokud se domníváte, že se jedná o chybu, kontaktujte podporu."
},
"billingPreviewInvoiceError": {
"message": "Při náhledu faktury došlo k chybě. Opakujte akci později."
},
"unverified": { "unverified": {
"message": "Neověřeno" "message": "Neověřeno"
}, },
@@ -10007,5 +10019,48 @@
}, },
"organizationNameMaxLength": { "organizationNameMaxLength": {
"message": "Název organizace nesmí přesáhnout 50 znaků." "message": "Název organizace nesmí přesáhnout 50 znaků."
},
"resellerRenewalWarning": {
"message": "Vaše předplatné se brzy obnoví. Chcete-li zajistit nepřerušenou službu, kontaktujte $RESELLER$ pro potvrzení obnovení před $RENEWAL_DATE$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"renewal_date": {
"content": "$2",
"example": "01/01/2024"
}
}
},
"resellerOpenInvoiceWarning": {
"message": "Faktura pro Vaše předplatné byla vystavena dne $ISSUED_DATE$. Chcete-li zajistit nepřerušovanou službu, kontaktujte $RESELLER$ pro potvrzení obnovení před $DUE_DATE$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"issued_date": {
"content": "$2",
"example": "01/01/2024"
},
"due_date": {
"content": "$3",
"example": "01/15/2024"
}
}
},
"resellerPastDueWarning": {
"message": "Faktura za Vaše předplatné nebyla zaplacena. Chcete-li zajistit nepřerušovanou službu, kontaktujte $RESELLER$ pro potvrzení obnovení před $GRACE_PERIOD_END$.",
"placeholders": {
"reseller": {
"content": "$1",
"example": "Reseller Name"
},
"grace_period_end": {
"content": "$2",
"example": "02/14/2024"
}
}
} }
} }

Some files were not shown because too many files have changed in this diff Show More