diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt index a27e52d22e..daf1ce408b 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt @@ -3,6 +3,7 @@ package com.tangem.tap.domain.userWalletList.implementation import com.tangem.common.* import com.tangem.domain.wallets.legacy.UserWalletsListError import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey @@ -56,8 +57,8 @@ internal class BiometricUserWalletsListManager( override val walletsCount: Int get() = state.value.userWallets.size - override suspend fun unlock(throwIfNotAllWalletsUnlocked: Boolean): CompletionResult { - return unlockWithBiometryInternal() + override suspend fun unlock(type: UnlockType): CompletionResult { + return unlockAndSetSelectedUserWallet(type) .mapFailure { error -> Timber.e(error, "Unable to unlock user wallets") if (error is UserWalletsListError) { @@ -66,16 +67,8 @@ internal class BiometricUserWalletsListManager( UserWalletsListError.UnableToUnlockUserWallets(error) } } - .map { - val userWallets = state.value.userWallets - - if (throwIfNotAllWalletsUnlocked && userWallets.any(UserWallet::isLocked)) { - Timber.e("Some user wallets remain locked") - throw UserWalletsListError.NotAllUserWalletsUnlocked - } - - val selectedUserWallet = selectedUserWalletSync - if (selectedUserWallet == null) { + .map { selectedUserWallet -> + if (selectedUserWallet == null || selectedUserWallet.isLocked) { Timber.e("Unable to find selected user wallet") throw UserWalletsListError.NoUserWalletSelected } else { @@ -186,99 +179,116 @@ internal class BiometricUserWalletsListManager( changeSelectedUserWallet: Boolean, canOverridePublicInfo: Boolean, ): CompletionResult { - return saveEncryptionKeyIfNotNull(userWallet) - .flatMap { sensitiveInformationRepository.save(userWallet, encryptionKey = it) } - .flatMap { publicInformationRepository.save(userWallet, canOverridePublicInfo) } - .map { - if (changeSelectedUserWallet) { - selectedUserWalletRepository.set(userWallet.walletId) - } - } - .flatMap { loadModels() } - .doOnSuccess { - state.update { prevState -> - prevState.copy( - selectedUserWalletId = if (changeSelectedUserWallet) { - userWallet.walletId - } else { - prevState.selectedUserWalletId - }, - isLocked = prevState.userWallets.any { it.isLocked }, - ) - } - } - } - - private suspend fun unlockWithBiometryInternal(): CompletionResult { - return keysRepository.getAll() - .map { keys -> - state.update { prevState -> - prevState.copy( - encryptionKeys = (keys + prevState.encryptionKeys).distinctBy { it.walletId }, - ) - } - } - .flatMap { loadModels() } - .map { - state.update { prevState -> - val hasLockedUserWallets = prevState.userWallets.any { it.isLocked } - prevState.copy(isLocked = hasLockedUserWallets) - } - } - } - - private suspend fun saveEncryptionKeyIfNotNull(userWallet: UserWallet): CompletionResult { val encryptionKey = userWallet.scanResponse.card.encryptionKey ?.let { UserWalletEncryptionKey(userWallet.walletId, it) } + ?: return CompletionResult.Success(Unit) // No encryption key, no need to save - return if (encryptionKey != null) { - keysRepository.save(encryptionKey) - .doOnSuccess { - state.update { prevState -> - prevState.copy( - encryptionKeys = prevState.encryptionKeys - .plus(encryptionKey) - .distinctBy { it.walletId }, - ) - } + return keysRepository.save(encryptionKey) + .flatMap { sensitiveInformationRepository.save(userWallet, encryptionKey = encryptionKey.encryptionKey) } + .flatMap { publicInformationRepository.save(userWallet, canOverridePublicInfo) } + .flatMap { + loadUserWallets( + encryptionKeys = state.value.encryptionKeys + .plus(encryptionKey) + .distinctBy(UserWalletEncryptionKey::walletId), + ) + } + .doOnSuccess { loadedState -> + if (changeSelectedUserWallet) { + selectedUserWalletRepository.set(userWallet.walletId) + + state.value = loadedState.copy( + selectedUserWalletId = userWallet.walletId, + ) + } else { + state.value = loadedState } - .map { encryptionKey.encryptionKey } - } else { - CompletionResult.Success(data = null) - } + } + .map { /* Type erasing */ } } - private suspend fun loadModels(): CompletionResult { - return getSavedUserWallets() - .map { userWallets -> - if (userWallets.isNotEmpty()) { - state.update { prevState -> - val wallets = (userWallets + prevState.userWallets).distinctBy { it.walletId } + private suspend fun unlockAndSetSelectedUserWallet(type: UnlockType): CompletionResult { + return keysRepository.getAll() + .flatMap { encryptionKeys -> + loadUserWallets( + encryptionKeys = state.value.encryptionKeys + .plus(encryptionKeys) + .distinctBy(UserWalletEncryptionKey::walletId), + ) + } + .map { loadedState -> + when (type) { + UnlockType.ALL -> { + if (loadedState.isLocked) { + Timber.e("Some user wallets remain locked") - prevState.copy( - userWallets = wallets, - selectedUserWalletId = findOrSetSelectedWalletId(prevState.selectedUserWalletId, wallets), + state.value = loadedState + + throw UserWalletsListError.NotAllUserWalletsUnlocked + } else { + val selectedWallet = findOrSetSelectedWallet( + state.value.selectedUserWalletId, + loadedState.userWallets, + ) + + state.value = loadedState.copy( + selectedUserWalletId = selectedWallet?.walletId, + ) + + selectedWallet + } + } + UnlockType.ANY -> { + val selectedWallet = findOrSetSelectedWallet( + state.value.selectedUserWalletId, + loadedState.userWallets, ) + + state.value = loadedState.copy( + selectedUserWalletId = selectedWallet?.walletId, + ) + + selectedWallet + } + UnlockType.ALL_WITHOUT_SELECT -> { + state.value = loadedState + + findSelectedUserWallet() } } } } - private suspend fun getSavedUserWallets(): CompletionResult> { + private suspend fun loadUserWallets(encryptionKeys: List): CompletionResult { return publicInformationRepository.getAll() .map { it.toUserWallets() } .flatMap { userWallets -> - sensitiveInformationRepository.getAll(state.value.encryptionKeys) + sensitiveInformationRepository.getAll(encryptionKeys) .map { walletIdToSensitiveInformation -> userWallets.updateWith(walletIdToSensitiveInformation) } } + .map { userWallets -> + val prevState = state.value + + if (userWallets.isNotEmpty()) { + val newUserWallets = (userWallets + prevState.userWallets) + .distinctBy(UserWallet::walletId) + + prevState.copy( + userWallets = newUserWallets, + isLocked = newUserWallets.any(UserWallet::isLocked), + ) + } else { + prevState + } + } } - private fun findOrSetSelectedWalletId( + private fun findOrSetSelectedWallet( prevSelectedWalletId: UserWalletId?, userWallets: List, - ): UserWalletId? { + ): UserWallet? { val selectedWalletId = prevSelectedWalletId ?: selectedUserWalletRepository.get() var possibleSelectedUserWallet = findSelectedUserWallet(userWallets, selectedWalletId) @@ -290,7 +300,7 @@ internal class BiometricUserWalletsListManager( } } - return possibleSelectedUserWallet?.walletId + return possibleSelectedUserWallet } private fun changeSelectedUserWalletIdIfNeeded(walletsIdsToRemove: List) { diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt index 6fccee5d59..e0e160a524 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt @@ -100,10 +100,10 @@ internal class GeneralUserWalletsListManager( return implementation.value.get(userWalletId) } - override suspend fun unlock(throwIfNotAllWalletsUnlocked: Boolean): CompletionResult { + override suspend fun unlock(type: UserWalletsListManager.Lockable.UnlockType): CompletionResult { val implementation = implementation.value return if (implementation is UserWalletsListManager.Lockable) { - implementation.unlock() + implementation.unlock(type) } else { error("RuntimeUserWalletsListManager is not lockable") } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DelegatedKeystoreManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DelegatedKeystoreManager.kt index f61d6df0d0..7e7a781189 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DelegatedKeystoreManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DelegatedKeystoreManager.kt @@ -14,7 +14,7 @@ internal class DelegatedKeystoreManager( override suspend fun get( masterKeyConfig: KeystoreManager.MasterKeyConfig, - keyAliases: Collection, + keyAliases: Set, ): Map { return keystoreManagerProvider().get(masterKeyConfig, keyAliases) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt index d04e0122b7..6f7c82d446 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt @@ -89,7 +89,6 @@ internal class BiometricUserWalletsKeysRepository( .doOnFailure { error -> when (error) { is TangemSdkError.KeystoreInvalidated -> { - // If the biometric cryptography key was invalidated, then delete all encryption keys getUserWalletsIds().forEach { userWalletId -> deleteEncryptionKey(userWalletId) } @@ -120,7 +119,7 @@ internal class BiometricUserWalletsKeysRepository( } private fun deleteEncryptionKey(userWalletId: UserWalletId) { - return authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name) + authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name) } private suspend fun getUserWalletsIds(): List { diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index 5e03eed1a5..ca7d00d461 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -14,6 +14,7 @@ import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.userwallets.UserWalletBuilder +import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType import com.tangem.domain.wallets.legacy.unlockIfLockable import com.tangem.tap.* import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter @@ -86,7 +87,7 @@ internal class WelcomeMiddleware { """.trimIndent(), ) - userWalletsListManager.unlockIfLockable() + userWalletsListManager.unlockIfLockable(type = UnlockType.ANY) .doOnFailure { error -> Timber.e(error, "Unable to unlock user wallets with biometrics") store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Error(error)) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt index 7211f73a66..8a63233bed 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt @@ -105,16 +105,42 @@ interface UserWalletsListManager { /** * Receive saved [UserWallet]s, populate [userWallets] flow with it and set [isLocked] as false. * - * @param throwIfNotAllWalletsUnlocked Indicates that the function must throw - * [UserWalletsListError.NotAllUserWalletsUnlocked] if not all user wallets are unlocked. + * @param type Defines the behavior of the operation. * * @return [CompletionResult] of operation, with selected [UserWallet] * or null if there is no selected [UserWallet] */ - suspend fun unlock(throwIfNotAllWalletsUnlocked: Boolean = false): CompletionResult + suspend fun unlock(type: UnlockType): CompletionResult /** Remove [UserWallet]s from [userWallets] and set [isLocked] as true */ fun lock() + + /** + * Defines the behavior of the [unlock] operation. + * */ + enum class UnlockType { + /** + * Ensures that all stored [UserWallet]s are unlocked, + * or throws [UserWalletsListError.NotAllUserWalletsUnlocked]. + * + * In this type [selectedUserWallet] is either a previously selected [UserWallet] or the first stored + * [UserWallet]. + * */ + ALL, + + /** + * Ensures that at least one stored [UserWallet] is unlocked, + * or throws [UserWalletsListError.NoUserWalletSelected]. + * + * In this type [selectedUserWallet] is the first stored and unlocked [UserWallet]. + * */ + ANY, + + /** + * Same as [ALL] type, but this type can not change [selectedUserWallet] while unlocking. + * */ + ALL_WITHOUT_SELECT, + } } // For provider diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt index aa65450d99..0003a1300d 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt @@ -1,6 +1,7 @@ package com.tangem.domain.wallets.legacy import com.tangem.common.CompletionResult +import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType import com.tangem.domain.wallets.models.UserWallet import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf @@ -43,8 +44,8 @@ val UserWalletsListManager.isLockedSync: Boolean * * @see UserWalletsListManager.Lockable.unlock * */ -suspend fun UserWalletsListManager.unlockIfLockable(): CompletionResult { - return asLockable()?.unlock() ?: CompletionResult.Failure(UserWalletsListError.UnableToUnlockUserWallets()) +suspend fun UserWalletsListManager.unlockIfLockable(type: UnlockType = UnlockType.ANY): CompletionResult { + return asLockable()?.unlock(type) ?: CompletionResult.Failure(UserWalletsListError.UnableToUnlockUserWallets()) } /** diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt index 5ae878661a..9b6bdf4997 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt @@ -5,6 +5,7 @@ import arrow.core.raise.either import arrow.core.raise.ensureNotNull import com.tangem.common.doOnFailure import com.tangem.domain.wallets.legacy.UserWalletsListError +import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.models.UnlockWalletsError @@ -18,27 +19,26 @@ import com.tangem.domain.wallets.models.UnlockWalletsError */ class UnlockWalletsUseCase(private val walletsStateHolder: WalletsStateHolder) { - suspend operator fun invoke(throwIfNotAllWalletsUnlocked: Boolean = false): Either = - either { - val userWalletsListManager = ensureNotNull( - value = walletsStateHolder.userWalletsListManager?.asLockable(), - raise = { - UnlockWalletsError.DataError( - cause = IllegalStateException("The lockable user wallets list manager could not be found"), - ) - }, - ) + suspend operator fun invoke(type: UnlockType = UnlockType.ANY): Either = either { + val userWalletsListManager = ensureNotNull( + value = walletsStateHolder.userWalletsListManager?.asLockable(), + raise = { + UnlockWalletsError.DataError( + cause = IllegalStateException("The lockable user wallets list manager could not be found"), + ) + }, + ) - userWalletsListManager.unlock(throwIfNotAllWalletsUnlocked) - .doOnFailure { error -> - val e = when (error) { - is UserWalletsListError.NoUserWalletSelected -> UnlockWalletsError.NoUserWalletSelected - is UserWalletsListError.NotAllUserWalletsUnlocked -> - UnlockWalletsError.NotAllUserWalletsUnlocked - else -> UnlockWalletsError.UnableToUnlockWallets - } - - raise(e) + userWalletsListManager.unlock(type) + .doOnFailure { error -> + val e = when (error) { + is UserWalletsListError.NoUserWalletSelected -> UnlockWalletsError.NoUserWalletSelected + is UserWalletsListError.NotAllUserWalletsUnlocked -> + UnlockWalletsError.NotAllUserWalletsUnlocked + else -> UnlockWalletsError.UnableToUnlockWallets } - } + + raise(e) + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index d44e4a04e5..f16f13dda2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -13,6 +13,7 @@ import com.tangem.domain.settings.RemindToRateAppLaterUseCase import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase import com.tangem.domain.tokens.FetchTokenListUseCase import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType import com.tangem.domain.wallets.models.UnlockWalletsError import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -135,7 +136,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( analyticsEventHandler.send(MainScreen.UnlockAllWithBiometrics) viewModelScope.launch(dispatchers.main) { - unlockWalletsUseCase(throwIfNotAllWalletsUnlocked = true) + unlockWalletsUseCase(type = UnlockType.ALL_WITHOUT_SELECT) .onRight { stateHolder.update(CloseBottomSheetTransformer(stateHolder.getSelectedWalletId())) } .onLeft(::handleUnlockWalletsError) } diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index a5b5769a89..7e5893d613 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -87,7 +87,7 @@ web3j = "4.10.1" # region Tangem tangemBlockchainSdk = "release-app_5.8-533" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.8-335" +tangemCardSdk = "release-app_5.8-336" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem