From 462c13643aaf41121d0dd84a39e252463c24222d Mon Sep 17 00:00:00 2001 From: Artem Chepurnoy Date: Thu, 15 Feb 2024 16:27:43 +0200 Subject: [PATCH] feat: Hide accounts from the Main screens #35 --- .../android/PasskeyGetUnlockActivity.kt | 11 +- .../autofill/KeyguardAutofillService.kt | 10 +- .../autofill/KeyguardCredentialService.kt | 12 +- .../keyguard/common/model/DProfile.kt | 5 + .../common/model/PutProfileHiddenRequest.kt | 5 + .../keyguard/common/usecase/GetCiphers.kt | 76 +++++++++++ .../common/usecase/PutProfileHidden.kt | 8 ++ .../keyguard/core/session/usecase/SubDI.kt | 5 + .../core/store/bitwarden/BitwardenProfile.kt | 2 + .../keyguard/feature/auth/AccountViewState.kt | 4 +- .../feature/auth/AccountViewStateProducer.kt | 124 +++++++++++++----- .../list/DuplicatesListStateProducer.kt | 15 ++- .../settings/accounts/AccountListViewModel.kt | 1 + .../accounts/component/AccountListItem.kt | 8 +- .../settings/accounts/model/AccountItem.kt | 1 + .../home/vault/component/VaultListItem.kt | 19 +-- .../vault/screen/VaultListStateProducer.kt | 10 +- .../vault/screen/VaultRecentStateProducer.kt | 11 +- .../feature/send/SendListStateProducer.kt | 22 ++-- .../feature/send/list/SendListScreen.kt | 7 +- .../watchtower/WatchtowerStateProducer.kt | 8 +- .../provider/bitwarden/api/SyncEngine.kt | 6 +- .../keyguard/provider/bitwarden/api/fff.kt | 11 ++ .../bitwarden/mapper/ProfileMapping.kt | 1 + .../bitwarden/usecase/PutProfileHiddenImpl.kt | 47 +++++++ .../usecase/util/ModifyProfileById.kt | 13 +- .../keyguard/ui/PasswordFilterItem.kt | 15 +++ .../commonMain/resources/MR/base/strings.xml | 2 + 28 files changed, 374 insertions(+), 85 deletions(-) create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/PutProfileHiddenRequest.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/PutProfileHidden.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/PutProfileHiddenImpl.kt diff --git a/common/src/androidMain/kotlin/com/artemchep/keyguard/android/PasskeyGetUnlockActivity.kt b/common/src/androidMain/kotlin/com/artemchep/keyguard/android/PasskeyGetUnlockActivity.kt index 6c377be6..c1aa1af8 100644 --- a/common/src/androidMain/kotlin/com/artemchep/keyguard/android/PasskeyGetUnlockActivity.kt +++ b/common/src/androidMain/kotlin/com/artemchep/keyguard/android/PasskeyGetUnlockActivity.kt @@ -26,7 +26,9 @@ import androidx.lifecycle.lifecycleScope import com.artemchep.keyguard.common.model.MasterSession import com.artemchep.keyguard.common.model.VaultState import com.artemchep.keyguard.common.usecase.GetCiphers +import com.artemchep.keyguard.common.usecase.GetProfiles import com.artemchep.keyguard.common.usecase.GetVaultSession +import com.artemchep.keyguard.common.usecase.filterHiddenProfiles import com.artemchep.keyguard.feature.keyguard.ManualAppScreen import com.artemchep.keyguard.feature.keyguard.ManualAppScreenOnCreate import com.artemchep.keyguard.feature.keyguard.ManualAppScreenOnLoading @@ -179,7 +181,14 @@ class PasskeyGetUnlockActivity : BaseActivity(), DIAware { } val ciphers = kotlin.run { val getCiphers = session.di.direct.instance() - getCiphers() + val getProfiles = session.di.direct.instance() + + val ciphersRawFlow = filterHiddenProfiles( + getProfiles = getProfiles, + getCiphers = getCiphers, + filter = null, + ) + ciphersRawFlow .first() } diff --git a/common/src/androidMain/kotlin/com/artemchep/keyguard/android/autofill/KeyguardAutofillService.kt b/common/src/androidMain/kotlin/com/artemchep/keyguard/android/autofill/KeyguardAutofillService.kt index acbfad14..0713af4c 100644 --- a/common/src/androidMain/kotlin/com/artemchep/keyguard/android/autofill/KeyguardAutofillService.kt +++ b/common/src/androidMain/kotlin/com/artemchep/keyguard/android/autofill/KeyguardAutofillService.kt @@ -65,10 +65,16 @@ class KeyguardAutofillService : AutofillService(), DIAware { when (session) { is MasterSession.Key -> { val getCiphers = session.di.direct.instance() - getCiphers() + val getProfiles = session.di.direct.instance() + val ciphersRawFlow = filterHiddenProfiles( + getProfiles = getProfiles, + getCiphers = getCiphers, + filter = null, + ) + ciphersRawFlow .map { ciphers -> ciphers - .filter { it.deletedDate == null } + .filter { !it.deleted } .right() } } diff --git a/common/src/androidMain/kotlin/com/artemchep/keyguard/android/autofill/KeyguardCredentialService.kt b/common/src/androidMain/kotlin/com/artemchep/keyguard/android/autofill/KeyguardCredentialService.kt index 45c26885..bff0dbb2 100644 --- a/common/src/androidMain/kotlin/com/artemchep/keyguard/android/autofill/KeyguardCredentialService.kt +++ b/common/src/androidMain/kotlin/com/artemchep/keyguard/android/autofill/KeyguardCredentialService.kt @@ -35,7 +35,9 @@ import com.artemchep.keyguard.common.io.launchIn import com.artemchep.keyguard.common.model.MasterSession import com.artemchep.keyguard.common.usecase.GetCanWrite import com.artemchep.keyguard.common.usecase.GetCiphers +import com.artemchep.keyguard.common.usecase.GetProfiles import com.artemchep.keyguard.common.usecase.GetVaultSession +import com.artemchep.keyguard.common.usecase.filterHiddenProfiles import com.artemchep.keyguard.feature.crashlytics.crashlyticsTap import com.artemchep.keyguard.platform.recordLog import kotlinx.coroutines.CoroutineScope @@ -156,11 +158,17 @@ class KeyguardCredentialService : CredentialProviderService(), DIAware { val cipherHistoryOpenedRepository = session.di.direct.instance() val getCiphers = session.di.direct.instance() + val getProfiles = session.di.direct.instance() - val ciphers = getCiphers() + val ciphersRawFlow = filterHiddenProfiles( + getProfiles = getProfiles, + getCiphers = getCiphers, + filter = null, + ) + val ciphers = ciphersRawFlow .map { ciphers -> ciphers - .filter { it.deletedDate == null } + .filter { !it.deleted } } .first() val response = ioEffect { diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/DProfile.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/DProfile.kt index 261c5f8c..db5efe85 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/DProfile.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/DProfile.kt @@ -14,6 +14,11 @@ data class DProfile( val accentColor: AccentColors, val name: String, val premium: Boolean, + /** + * `true` if the account should be hidden from the main screens and + * not used during the autofill process, `false` otherwise. + */ + val hidden: Boolean, val securityStamp: String?, val twoFactorEnabled: Boolean, val masterPasswordHint: String?, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/PutProfileHiddenRequest.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/PutProfileHiddenRequest.kt new file mode 100644 index 00000000..f3c77474 --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/PutProfileHiddenRequest.kt @@ -0,0 +1,5 @@ +package com.artemchep.keyguard.common.model + +data class PutProfileHiddenRequest( + val patch: Map, +) diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/GetCiphers.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/GetCiphers.kt index 4658aa25..f99c31f5 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/GetCiphers.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/GetCiphers.kt @@ -1,10 +1,86 @@ package com.artemchep.keyguard.common.usecase +import com.artemchep.keyguard.common.model.DFilter import com.artemchep.keyguard.common.model.DSecret +import com.artemchep.keyguard.common.model.DSend +import com.artemchep.keyguard.common.model.DSendFilter +import kotlinx.collections.immutable.toPersistentSet import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map /** * Provides a list of all available on the * device accounts. */ interface GetCiphers : () -> Flow> + +fun filterHiddenProfiles( + getCiphers: GetCiphers, + getProfiles: GetProfiles, + filter: DFilter? = null, +): Flow> { + val shouldFilter = filter + ?.let { f -> + // If we have a pre-set filter that specifies an + // identifier of the folder or collection or account etc, + // then we ignore the hidden account setting. + DFilter.findOne(f) { true } == null + } + ?: true + return if (shouldFilter) { + _filterHiddenProfiles( + getItems = getCiphers, + getProfiles = getProfiles, + getAccount = { it.accountId }, + ) + } else { + getCiphers() + } +} + +fun filterHiddenProfiles( + getSends: GetSends, + getProfiles: GetProfiles, + filter: DSendFilter? = null, +): Flow> { + val shouldFilter = filter + ?.let { f -> + // If we have a pre-set filter that specifies an + // identifier of the folder or collection or account etc, + // then we ignore the hidden account setting. + DSendFilter.findOne(f) { true } == null + } + ?: true + return if (shouldFilter) { + _filterHiddenProfiles( + getItems = getSends, + getProfiles = getProfiles, + getAccount = { it.accountId }, + ) + } else { + getSends() + } +} + +private inline fun _filterHiddenProfiles( + getItems: () -> Flow>, + getProfiles: GetProfiles, + crossinline getAccount: (T) -> String, +): Flow> { + val ccc = getProfiles() + .map { profiles -> + profiles + .asSequence() + .filter { it.hidden } + .map { it.accountId } + .toPersistentSet() + } + .distinctUntilChanged() + return getItems() + .combine(ccc) { items, hiddenAccountIds -> + items + .filter { getAccount(it) !in hiddenAccountIds } + } +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/PutProfileHidden.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/PutProfileHidden.kt new file mode 100644 index 00000000..4c1b36e1 --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/PutProfileHidden.kt @@ -0,0 +1,8 @@ +package com.artemchep.keyguard.common.usecase + +import com.artemchep.keyguard.common.io.IO +import com.artemchep.keyguard.common.model.PutProfileHiddenRequest + +interface PutProfileHidden : ( + PutProfileHiddenRequest, +) -> IO diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/core/session/usecase/SubDI.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/core/session/usecase/SubDI.kt index f0ae5056..1877890a 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/core/session/usecase/SubDI.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/core/session/usecase/SubDI.kt @@ -87,6 +87,7 @@ import com.artemchep.keyguard.common.usecase.PatchWatchtowerAlertCipher import com.artemchep.keyguard.common.usecase.PutAccountColorById import com.artemchep.keyguard.common.usecase.PutAccountMasterPasswordHintById import com.artemchep.keyguard.common.usecase.PutAccountNameById +import com.artemchep.keyguard.common.usecase.PutProfileHidden import com.artemchep.keyguard.common.usecase.RePromptCipherById import com.artemchep.keyguard.common.usecase.RemoveAccountById import com.artemchep.keyguard.common.usecase.RemoveAccounts @@ -187,6 +188,7 @@ import com.artemchep.keyguard.provider.bitwarden.usecase.PatchWatchtowerAlertCip import com.artemchep.keyguard.provider.bitwarden.usecase.PutAccountColorByIdImpl import com.artemchep.keyguard.provider.bitwarden.usecase.PutAccountMasterPasswordHintByIdImpl import com.artemchep.keyguard.provider.bitwarden.usecase.PutAccountNameByIdImpl +import com.artemchep.keyguard.provider.bitwarden.usecase.PutProfileHiddenImpl import com.artemchep.keyguard.provider.bitwarden.usecase.RePromptCipherByIdImpl import com.artemchep.keyguard.provider.bitwarden.usecase.RemoveAccountByIdImpl import com.artemchep.keyguard.provider.bitwarden.usecase.RemoveAccountsImpl @@ -417,6 +419,9 @@ fun DI.Builder.createSubDi2( bindSingleton { PutAccountNameByIdImpl(this) } + bindSingleton { + PutProfileHiddenImpl(this) + } bindSingleton { GetGeneratorHistoryImpl( directDI = this, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/core/store/bitwarden/BitwardenProfile.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/core/store/bitwarden/BitwardenProfile.kt index 285dbc8a..94529b31 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/core/store/bitwarden/BitwardenProfile.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/core/store/bitwarden/BitwardenProfile.kt @@ -21,6 +21,8 @@ data class BitwardenProfile( val email: String, val emailVerified: Boolean, val premium: Boolean, + // Keyguard-specific field + val hidden: Boolean = false, val securityStamp: String = "", val twoFactorEnabled: Boolean, val masterPasswordHint: String?, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/auth/AccountViewState.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/auth/AccountViewState.kt index c36aafab..e78ef1a6 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/auth/AccountViewState.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/auth/AccountViewState.kt @@ -5,7 +5,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import arrow.optics.optics import com.artemchep.keyguard.common.model.DAccount import com.artemchep.keyguard.feature.home.vault.model.VaultViewItem -import com.artemchep.keyguard.ui.FlatItemAction +import com.artemchep.keyguard.ui.ContextItem @Immutable @optics @@ -26,7 +26,7 @@ data class AccountViewState( data class Data( val data: DAccount, val items: List, - val actions: List, + val actions: List, val primaryAction: PrimaryAction? = null, val onOpenWebVault: (() -> Unit)? = null, ) : Content { diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/auth/AccountViewStateProducer.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/auth/AccountViewStateProducer.kt index 9a8781c7..243b2445 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/auth/AccountViewStateProducer.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/auth/AccountViewStateProducer.kt @@ -11,14 +11,18 @@ import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.Fingerprint import androidx.compose.material.icons.outlined.Folder import androidx.compose.material.icons.outlined.HelpOutline +import androidx.compose.material.icons.outlined.HideSource +import androidx.compose.material.icons.outlined.Keyboard import androidx.compose.material.icons.outlined.Login import androidx.compose.material.icons.outlined.Logout import androidx.compose.material.icons.outlined.VerifiedUser +import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.material3.Badge import androidx.compose.material3.BadgedBox import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -36,6 +40,7 @@ import com.artemchep.keyguard.common.model.DAccount import com.artemchep.keyguard.common.model.DFilter import com.artemchep.keyguard.common.model.DMeta import com.artemchep.keyguard.common.model.DProfile +import com.artemchep.keyguard.common.model.PutProfileHiddenRequest import com.artemchep.keyguard.common.model.firstOrNull import com.artemchep.keyguard.common.service.clipboard.ClipboardService import com.artemchep.keyguard.common.usecase.CopyText @@ -52,6 +57,7 @@ import com.artemchep.keyguard.common.usecase.GetProfiles import com.artemchep.keyguard.common.usecase.PutAccountColorById import com.artemchep.keyguard.common.usecase.PutAccountMasterPasswordHintById import com.artemchep.keyguard.common.usecase.PutAccountNameById +import com.artemchep.keyguard.common.usecase.PutProfileHidden import com.artemchep.keyguard.common.usecase.QueueSyncById import com.artemchep.keyguard.common.usecase.RemoveAccountById import com.artemchep.keyguard.common.usecase.SupervisorRead @@ -117,6 +123,7 @@ fun accountState( putAccountNameById = instance(), putAccountColorById = instance(), putAccountMasterPasswordHintById = instance(), + putProfileHidden = instance(), getAccounts = instance(), getProfiles = instance(), getCiphers = instance(), @@ -148,6 +155,7 @@ fun accountState( putAccountNameById: PutAccountNameById, putAccountColorById: PutAccountColorById, putAccountMasterPasswordHintById: PutAccountMasterPasswordHintById, + putProfileHidden: PutProfileHidden, getAccounts: GetAccounts, getProfiles: GetProfiles, getCiphers: GetCiphers, @@ -203,6 +211,16 @@ fun accountState( navigate(intent) } + fun doHideProfileById(profileId: String, hidden: Boolean) { + val request = PutProfileHiddenRequest( + patch = mapOf( + profileId to hidden, + ), + ) + putProfileHidden(request) + .launchIn(appScope) + } + val accountFlow = getAccounts() .map { it.firstOrNull(accountId) } launchAutoPopSelfHandler(accountFlow) @@ -317,48 +335,88 @@ fun accountState( getGravatarUrl = getGravatarUrl, ).toList() } + val actionsFlow = combine( + accountFlow, + profileFlow, + ) { accountOrNull, profileOrNull -> + val syncing = false + val busy = false + buildContextItems { + section { + if (accountOrNull == null) { + return@section + } + + this += FlatItemAction( + leading = { + SyncIcon(rotating = syncing) + }, + title = translate(Res.strings.sync), + onClick = if (busy) { + null + } else { + // on click listener + ::doSyncAccountById.partially1(accountOrNull.id) + }, + ) + } + section { + if (profileOrNull == null) { + return@section + } + + val hidden = profileOrNull.hidden + + val onCheckedChange = ::doHideProfileById + .partially1(profileOrNull.profileId) + this += FlatItemAction( + leading = { + Icon( + Icons.Outlined.VisibilityOff, + null, + ) + }, + trailing = { + Switch( + checked = hidden, + onCheckedChange = onCheckedChange, + ) + }, + title = translate(Res.strings.account_action_hide_title), + text = translate(Res.strings.account_action_hide_text), + onClick = onCheckedChange.partially1(!hidden), + ) + } + section { + if (accountOrNull == null) { + return@section + } + + this += FlatItemAction( + icon = Icons.Outlined.Logout, + title = translate(Res.strings.account_action_sign_out_title), + onClick = if (busy) { + null + } else { + // on click listener + ::doRemoveAccountById.partially1(accountOrNull.id) + }, + ) + } + } + } combine( accountFlow, + actionsFlow, itemsFlow, primaryActionFlow, - ) { accountOrNull, items, primaryAction -> + ) { accountOrNull, actions, items, primaryAction -> if (accountOrNull == null) { return@combine AccountViewState.Content.NotFound } - - val syncing = false - val busy = false - - val actionSync = - FlatItemAction( - leading = { - SyncIcon(rotating = syncing) - }, - title = translate(Res.strings.sync), - onClick = if (busy) { - null - } else { - // on click listener - ::doSyncAccountById.partially1(accountOrNull.id) - }, - ) - val actionRemove = - FlatItemAction( - icon = Icons.Outlined.Logout, - title = translate(Res.strings.account_action_sign_out_title), - onClick = if (busy) { - null - } else { - // on click listener - ::doRemoveAccountById.partially1(accountOrNull.id) - }, - ) AccountViewState.Content.Data( data = accountOrNull, - actions = listOf( - actionSync, - actionRemove, - ), + actions = actions, items = items, primaryAction = primaryAction, onOpenWebVault = { diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/duplicates/list/DuplicatesListStateProducer.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/duplicates/list/DuplicatesListStateProducer.kt index bb7f638d..7623f119 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/duplicates/list/DuplicatesListStateProducer.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/duplicates/list/DuplicatesListStateProducer.kt @@ -24,18 +24,17 @@ import com.artemchep.keyguard.common.usecase.GetCiphers import com.artemchep.keyguard.common.usecase.GetCollections import com.artemchep.keyguard.common.usecase.GetConcealFields import com.artemchep.keyguard.common.usecase.GetOrganizations +import com.artemchep.keyguard.common.usecase.GetProfiles import com.artemchep.keyguard.common.usecase.GetTotpCode import com.artemchep.keyguard.common.usecase.GetWebsiteIcons +import com.artemchep.keyguard.common.usecase.filterHiddenProfiles import com.artemchep.keyguard.common.util.flow.persistingStateIn import com.artemchep.keyguard.feature.attachments.SelectableItemState import com.artemchep.keyguard.feature.attachments.SelectableItemStateRaw import com.artemchep.keyguard.feature.confirmation.elevatedaccess.createElevatedAccessDialogIntent import com.artemchep.keyguard.feature.duplicates.DuplicatesRoute import com.artemchep.keyguard.feature.generator.history.mapLatestScoped -import com.artemchep.keyguard.feature.generator.wordlist.WordlistsRoute -import com.artemchep.keyguard.feature.home.vault.component.VaultListItem import com.artemchep.keyguard.feature.home.vault.model.VaultItem2 -import com.artemchep.keyguard.feature.home.vault.model.VaultItemIcon import com.artemchep.keyguard.feature.home.vault.screen.VaultViewRoute import com.artemchep.keyguard.feature.home.vault.screen.toVaultListItem import com.artemchep.keyguard.feature.home.vault.screen.verify @@ -70,7 +69,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import org.kodein.di.DirectDI import org.kodein.di.compose.localDI @@ -103,6 +101,7 @@ fun produceDuplicatesListState( getOrganizations = instance(), getCollections = instance(), getCiphers = instance(), + getProfiles = instance(), getCanWrite = instance(), cipherToolbox = instance(), cipherDuplicatesCheck = instance(), @@ -121,6 +120,7 @@ fun produceDuplicatesListState( getOrganizations: GetOrganizations, getCollections: GetCollections, getCiphers: GetCiphers, + getProfiles: GetProfiles, getCanWrite: GetCanWrite, cipherToolbox: CipherToolbox, cipherDuplicatesCheck: CipherDuplicatesCheck, @@ -203,7 +203,12 @@ fun produceDuplicatesListState( if (r == 0) r = a.id.compareTo(b.id) r } - val ciphersFlow = getCiphers() + val ciphersRawFlow = filterHiddenProfiles( + getProfiles = getProfiles, + getCiphers = getCiphers, + filter = args.filter, + ) + val ciphersFlow = ciphersRawFlow .map { ciphers -> ciphers .filter { it.deletedDate == null } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/accounts/AccountListViewModel.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/accounts/AccountListViewModel.kt index 1211dde9..f1818855 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/accounts/AccountListViewModel.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/accounts/AccountListViewModel.kt @@ -254,6 +254,7 @@ fun accountListScreenState( title = AnnotatedString(it.username), text = it.host, error = error, + hidden = profile?.hidden == true, syncing = syncing, selecting = selectionMode, actions = listOf(), diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/accounts/component/AccountListItem.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/accounts/component/AccountListItem.kt index 3bf2940e..7cd4aaf5 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/accounts/component/AccountListItem.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/accounts/component/AccountListItem.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ErrorOutline +import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.material.icons.outlined.Warning import androidx.compose.material3.Checkbox import androidx.compose.material3.ExperimentalMaterial3Api @@ -32,6 +33,7 @@ import androidx.compose.ui.unit.dp import com.artemchep.keyguard.feature.home.settings.accounts.model.AccountItem import com.artemchep.keyguard.feature.home.vault.component.Section import com.artemchep.keyguard.feature.home.vault.component.rememberSecretAccentColor +import com.artemchep.keyguard.ui.AvatarBadgeIcon import com.artemchep.keyguard.ui.AvatarBuilder import com.artemchep.keyguard.ui.ExpandedIfNotEmptyForRow import com.artemchep.keyguard.ui.FlatItemLayout @@ -85,7 +87,11 @@ fun AccountListItemText( accent = accent, active = true, badge = { - // Do nothing. + if (item.hidden) { + AvatarBadgeIcon( + imageVector = Icons.Outlined.VisibilityOff, + ) + } }, ) }, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/accounts/model/AccountItem.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/accounts/model/AccountItem.kt index fbdeb8f0..452d2847 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/accounts/model/AccountItem.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/accounts/model/AccountItem.kt @@ -32,6 +32,7 @@ sealed interface AccountItem { val title: AnnotatedString, val text: String?, val error: Boolean = false, + val hidden: Boolean, val syncing: Boolean = false, val selecting: Boolean = false, val actionNeeded: Boolean, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultListItem.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultListItem.kt index fdb69d80..7a116194 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultListItem.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultListItem.kt @@ -74,6 +74,7 @@ import com.artemchep.keyguard.feature.home.vault.model.VaultItemIcon import com.artemchep.keyguard.feature.localization.textResource import com.artemchep.keyguard.feature.twopane.LocalHasDetailPane import com.artemchep.keyguard.res.Res +import com.artemchep.keyguard.ui.AvatarBadgeIcon import com.artemchep.keyguard.ui.AvatarBuilder import com.artemchep.keyguard.ui.DisabledEmphasisAlpha import com.artemchep.keyguard.ui.DropdownMenuItemFlat @@ -840,30 +841,18 @@ fun AccountListItemTextIcon( active = item.source.service.remote != null, badge = { if (item.favourite) { - Icon( - modifier = Modifier - .size(16.dp) - .padding(1.dp), + AvatarBadgeIcon( imageVector = Icons.Outlined.KeyguardFavourite, - contentDescription = null, ) } if (item.source.reprompt) { - Icon( - modifier = Modifier - .size(16.dp) - .padding(1.dp), + AvatarBadgeIcon( imageVector = Icons.Outlined.Lock, - contentDescription = null, ) } if (item.attachments) { - Icon( - modifier = Modifier - .size(16.dp) - .padding(1.dp), + AvatarBadgeIcon( imageVector = Icons.Outlined.KeyguardAttachment, - contentDescription = null, ) } }, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultListStateProducer.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultListStateProducer.kt index a039baeb..85e0fc73 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultListStateProducer.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultListStateProducer.kt @@ -60,6 +60,7 @@ import com.artemchep.keyguard.common.usecase.PasskeyTargetCheck import com.artemchep.keyguard.common.usecase.QueueSyncAll import com.artemchep.keyguard.common.usecase.RenameFolderById import com.artemchep.keyguard.common.usecase.SupervisorRead +import com.artemchep.keyguard.common.usecase.filterHiddenProfiles import com.artemchep.keyguard.common.util.StringComparatorIgnoreCase import com.artemchep.keyguard.common.util.flow.EventFlow import com.artemchep.keyguard.common.util.flow.persistingStateIn @@ -314,7 +315,12 @@ fun vaultListScreenState( } val copy = copy(clipboardService) - val ciphersRawFlow = getCiphers() + + val ciphersRawFlow = filterHiddenProfiles( + getProfiles = getProfiles, + getCiphers = getCiphers, + filter = args.filter, + ) val querySink = mutablePersistedFlow("query") { "" } val queryState = mutableComposeState(querySink) @@ -326,7 +332,7 @@ fun vaultListScreenState( val selectionHandle = selectionHandle("selection") // Automatically remove selection from ciphers // that do not exist anymore. - getCiphers() + ciphersRawFlow .onEach { ciphers -> val selectedItemIds = selectionHandle.idsFlow.value val filteredSelectedItemIds = selectedItemIds diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultRecentStateProducer.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultRecentStateProducer.kt index 58fe64a1..19b644d9 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultRecentStateProducer.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultRecentStateProducer.kt @@ -20,11 +20,13 @@ import com.artemchep.keyguard.common.usecase.GetConcealFields import com.artemchep.keyguard.common.usecase.GetFolders import com.artemchep.keyguard.common.usecase.GetOrganizations import com.artemchep.keyguard.common.usecase.GetPasswordStrength +import com.artemchep.keyguard.common.usecase.GetProfiles import com.artemchep.keyguard.common.usecase.GetSuggestions import com.artemchep.keyguard.common.usecase.GetTotpCode import com.artemchep.keyguard.common.usecase.GetWebsiteIcons import com.artemchep.keyguard.common.usecase.QueueSyncAll import com.artemchep.keyguard.common.usecase.SupervisorRead +import com.artemchep.keyguard.common.usecase.filterHiddenProfiles import com.artemchep.keyguard.common.util.flow.EventFlow import com.artemchep.keyguard.feature.attachments.SelectableItemState import com.artemchep.keyguard.feature.home.vault.model.VaultItem2 @@ -58,6 +60,7 @@ fun vaultRecentScreenState( getAccounts = instance(), getCanWrite = instance(), getCiphers = instance(), + getProfiles = instance(), getFolders = instance(), getCollections = instance(), getOrganizations = instance(), @@ -84,6 +87,7 @@ fun vaultRecentScreenState( getAccounts: GetAccounts, getCanWrite: GetCanWrite, getCiphers: GetCiphers, + getProfiles: GetProfiles, getFolders: GetFolders, getCollections: GetCollections, getOrganizations: GetOrganizations, @@ -152,6 +156,11 @@ fun vaultRecentScreenState( val websiteIcons: Boolean, ) + val ciphersRawFlow = filterHiddenProfiles( + getProfiles = getProfiles, + getCiphers = getCiphers, + filter = null, + ) val configFlow = combine( getConcealFields(), getAppIcons(), @@ -182,7 +191,7 @@ fun vaultRecentScreenState( .map { it.cipherId } .toSet() } - .combine(getCiphers()) { recents, ciphers -> + .combine(ciphersRawFlow) { recents, ciphers -> recents .mapNotNull { recentId -> ciphers diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/send/SendListStateProducer.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/send/SendListStateProducer.kt index 128ac0a9..c48169e7 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/send/SendListStateProducer.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/send/SendListStateProducer.kt @@ -21,7 +21,6 @@ import com.artemchep.keyguard.common.model.AccountTask import com.artemchep.keyguard.common.model.DSecret import com.artemchep.keyguard.common.model.DSend import com.artemchep.keyguard.common.model.iconImageVector -import com.artemchep.keyguard.common.model.title import com.artemchep.keyguard.common.model.titleH import com.artemchep.keyguard.common.service.clipboard.ClipboardService import com.artemchep.keyguard.common.usecase.CipherToolbox @@ -30,11 +29,13 @@ import com.artemchep.keyguard.common.usecase.DateFormatter import com.artemchep.keyguard.common.usecase.GetAccounts import com.artemchep.keyguard.common.usecase.GetAppIcons import com.artemchep.keyguard.common.usecase.GetCanWrite +import com.artemchep.keyguard.common.usecase.GetProfiles import com.artemchep.keyguard.common.usecase.GetSends import com.artemchep.keyguard.common.usecase.GetSuggestions import com.artemchep.keyguard.common.usecase.GetWebsiteIcons import com.artemchep.keyguard.common.usecase.QueueSyncAll import com.artemchep.keyguard.common.usecase.SupervisorRead +import com.artemchep.keyguard.common.usecase.filterHiddenProfiles import com.artemchep.keyguard.common.util.flow.EventFlow import com.artemchep.keyguard.common.util.flow.persistingStateIn import com.artemchep.keyguard.feature.attachments.SelectableItemState @@ -99,7 +100,6 @@ import org.kodein.di.DirectDI import org.kodein.di.compose.localDI import org.kodein.di.direct import org.kodein.di.instance -import kotlin.time.ExperimentalTime import kotlin.time.measureTimedValue @LeParcelize @@ -151,6 +151,7 @@ fun sendListScreenState( getAccounts = instance(), getCanWrite = instance(), getSends = instance(), + getProfiles = instance(), getAppIcons = instance(), getWebsiteIcons = instance(), toolbox = instance(), @@ -173,6 +174,7 @@ fun sendListScreenState( getAccounts: GetAccounts, getCanWrite: GetCanWrite, getSends: GetSends, + getProfiles: GetProfiles, getAppIcons: GetAppIcons, getWebsiteIcons: GetWebsiteIcons, toolbox: CipherToolbox, @@ -195,15 +197,11 @@ fun sendListScreenState( } val copy = copy(clipboardService) - val ciphersRawFlow = getSends() -// .map { l -> -// Log.e("???","lol ciphers") -// (0..100).flatMap { l }.map { -// it.copy(id = UUID.randomUUID().toString()) -// }.also { -// Log.d("??? search", "${it.size} items") -// } -// } + val ciphersRawFlow = filterHiddenProfiles( + getProfiles = getProfiles, + getSends = getSends, + filter = null, + ) val querySink = mutablePersistedFlow("query") { "" } val queryState = mutableComposeState(querySink) @@ -215,7 +213,7 @@ fun sendListScreenState( val selectionHandle = selectionHandle("selection") // Automatically remove selection from ciphers // that do not exist anymore. - getSends() + ciphersRawFlow .onEach { ciphers -> val selectedItemIds = selectionHandle.idsFlow.value val filteredSelectedItemIds = selectedItemIds diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/send/list/SendListScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/send/list/SendListScreen.kt index 96604924..d535e9c2 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/send/list/SendListScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/send/list/SendListScreen.kt @@ -70,6 +70,7 @@ import com.artemchep.keyguard.feature.send.view.SendViewRoute import com.artemchep.keyguard.feature.twopane.LocalHasDetailPane import com.artemchep.keyguard.feature.twopane.TwoPaneScreen import com.artemchep.keyguard.res.Res +import com.artemchep.keyguard.ui.AvatarBadgeIcon import com.artemchep.keyguard.ui.AvatarBuilder import com.artemchep.keyguard.ui.CollectedEffect import com.artemchep.keyguard.ui.Compose @@ -674,12 +675,8 @@ fun AccountListItemTextIcon( active = active, badge = { if (item.hasPassword) { - Icon( - modifier = Modifier - .size(16.dp) - .padding(1.dp), + AvatarBadgeIcon( imageVector = Icons.Outlined.Key, - contentDescription = null, ) } }, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/watchtower/WatchtowerStateProducer.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/watchtower/WatchtowerStateProducer.kt index d9a89dbe..046c344d 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/watchtower/WatchtowerStateProducer.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/watchtower/WatchtowerStateProducer.kt @@ -16,6 +16,7 @@ import com.artemchep.keyguard.common.usecase.GetCollections import com.artemchep.keyguard.common.usecase.GetFolders import com.artemchep.keyguard.common.usecase.GetOrganizations import com.artemchep.keyguard.common.usecase.GetProfiles +import com.artemchep.keyguard.common.usecase.filterHiddenProfiles import com.artemchep.keyguard.common.util.flow.persistingStateIn import com.artemchep.keyguard.feature.crashlytics.crashlyticsMap import com.artemchep.keyguard.feature.duplicates.DuplicatesRoute @@ -101,7 +102,12 @@ fun produceWatchtowerState( PersistedStorage.InDisk(disk) } - val ciphersFlow = getCiphers() + val ciphersRawFlow = filterHiddenProfiles( + getProfiles = getProfiles, + getCiphers = getCiphers, + filter = null, + ) + val ciphersFlow = ciphersRawFlow .map { secrets -> secrets .filter { secret -> !secret.deleted } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/SyncEngine.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/SyncEngine.kt index b87a6856..083cc1a1 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/SyncEngine.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/SyncEngine.kt @@ -190,7 +190,11 @@ class SyncEngine( } // Insert updated profile. - if (existingProfile?.data_ != newProfile) { + val newMergedProfile = merge( + remote = newProfile, + local = existingProfile?.data_, + ) + if (newMergedProfile != existingProfile?.data_) { profileDao.insert( profileId = newProfile.profileId, accountId = newProfile.accountId, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/fff.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/fff.kt index 0597e909..ff453eda 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/fff.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/fff.kt @@ -14,6 +14,7 @@ import com.artemchep.keyguard.common.io.parallel import com.artemchep.keyguard.common.service.logging.LogLevel import com.artemchep.keyguard.common.usecase.GetPasswordStrength import com.artemchep.keyguard.core.store.bitwarden.BitwardenCipher +import com.artemchep.keyguard.core.store.bitwarden.BitwardenProfile import com.artemchep.keyguard.core.store.bitwarden.BitwardenService import com.artemchep.keyguard.provider.bitwarden.sync.SyncManager import kotlinx.coroutines.Dispatchers @@ -92,6 +93,16 @@ suspend fun merge( ) } +suspend fun merge( + remote: BitwardenProfile, + local: BitwardenProfile?, +): BitwardenProfile { + val hidden = local?.hidden == true + return remote.copy( + hidden = hidden, + ) +} + interface RemotePutScope { fun updateRemoteModel(remote: Remote) } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/mapper/ProfileMapping.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/mapper/ProfileMapping.kt index 446b3353..cde7cc16 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/mapper/ProfileMapping.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/mapper/ProfileMapping.kt @@ -47,6 +47,7 @@ fun BitwardenProfile.toDomain( accountUrl = accountUrl, name = name, premium = premium, + hidden = hidden, securityStamp = securityStamp, twoFactorEnabled = twoFactorEnabled, masterPasswordHint = masterPasswordHint, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/PutProfileHiddenImpl.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/PutProfileHiddenImpl.kt new file mode 100644 index 00000000..a3268680 --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/PutProfileHiddenImpl.kt @@ -0,0 +1,47 @@ +package com.artemchep.keyguard.provider.bitwarden.usecase + +import com.artemchep.keyguard.common.io.IO +import com.artemchep.keyguard.common.io.flatten +import com.artemchep.keyguard.common.io.ioEffect +import com.artemchep.keyguard.common.io.map +import com.artemchep.keyguard.common.model.PutProfileHiddenRequest +import com.artemchep.keyguard.common.usecase.PutProfileHidden +import com.artemchep.keyguard.core.store.bitwarden.BitwardenProfile +import com.artemchep.keyguard.core.store.bitwarden.hidden +import com.artemchep.keyguard.provider.bitwarden.usecase.util.ModifyProfileById +import org.kodein.di.DirectDI +import org.kodein.di.instance + +/** + * @author Artem Chepurnyi + */ +class PutProfileHiddenImpl( + private val modifyProfileById: ModifyProfileById, +) : PutProfileHidden { + constructor(directDI: DirectDI) : this( + modifyProfileById = directDI.instance(), + ) + + override fun invoke( + request: PutProfileHiddenRequest, + ): IO = ioEffect { + val profileIds = request.patch.keys + modifyProfileById( + profileIds, + ) { model -> + var new = model + val hidden = request.patch[model.profileId] + ?: return@modifyProfileById new + new = new.copy( + data_ = BitwardenProfile.hidden.set(new.data_, hidden), + ) + new + } + // Report that we have actually modified the + // profiles. + .map { changedCipherIds -> + true + } + } + .flatten() +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/util/ModifyProfileById.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/util/ModifyProfileById.kt index a758e182..a809d7e8 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/util/ModifyProfileById.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/util/ModifyProfileById.kt @@ -23,6 +23,7 @@ class ModifyProfileById( operator fun invoke( profileIds: Set, + checkIfChanged: Boolean = true, transform: suspend (Profile) -> Profile, ): IO = modifyDatabase { database -> val dao = database.profileQueries @@ -33,12 +34,20 @@ class ModifyProfileById( .filter { it.profileId in profileIds } - .map { model -> + .mapNotNull { model -> var new = model new = transform(new) + // If the profile was not changed, then we do not need to + // update it in the database. + if (checkIfChanged && new == model) { + return@mapNotNull null + } + new } - require(models.isNotEmpty()) + if (models.isEmpty()) { + return@modifyDatabase ModifyDatabase.Result.unit() + } dao.transaction { models.forEach { profile -> dao.insert( diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/PasswordFilterItem.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/PasswordFilterItem.kt index 92397fc5..5a40d79f 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/PasswordFilterItem.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/PasswordFilterItem.kt @@ -27,6 +27,7 @@ import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ContentCopy +import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.material3.DropdownMenu import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -707,3 +708,17 @@ fun AvatarBuilder( } } } + +@Composable +fun AvatarBadgeIcon( + modifier: Modifier = Modifier, + imageVector: ImageVector, +) { + Icon( + modifier = modifier + .size(16.dp) + .padding(1.dp), + imageVector = imageVector, + contentDescription = null, + ) +} diff --git a/common/src/commonMain/resources/MR/base/strings.xml b/common/src/commonMain/resources/MR/base/strings.xml index 1e58ecba..0eb9df3a 100644 --- a/common/src/commonMain/resources/MR/base/strings.xml +++ b/common/src/commonMain/resources/MR/base/strings.xml @@ -200,6 +200,8 @@ Change master password hint Sign out Sign in + Hide items + Hide the items from the main screens of the app Visit Web vault to verify your email address Upgrade your account to a premium membership and unlock some great additional features Two-factor authentication