Updated on 2026-08-14

This commit is contained in:
Tangem 2023-10-24 13:04:14 +03:00
parent 85c6044602
commit d470f278fc
8 changed files with 108 additions and 73 deletions

View file

@ -1,7 +1,6 @@
package com.tangem.tap.domain.userWalletList.implementation
import com.tangem.common.*
import com.tangem.common.extensions.guard
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
@ -57,12 +56,29 @@ internal class BiometricUserWalletsListManager(
override val walletsCount: Int
get() = state.value.userWallets.size
override suspend fun unlock(): CompletionResult<UserWallet> {
return unlockWithBiometryInternal().mapUnlockResult()
}
override suspend fun unlock(throwIfNotAllWalletsUnlocked: Boolean): CompletionResult<UserWallet> {
return unlockWithBiometryInternal()
.mapFailure { error ->
if (error is UserWalletsListError) {
error
} else {
UserWalletsListError.UnableToUnlockUserWallets(error)
}
}
.map {
if (throwIfNotAllWalletsUnlocked && state.value.userWallets.any(UserWallet::isLocked)) {
Timber.e("Not all user wallets have been unlocked")
throw UserWalletsListError.NotAllUserWalletsUnlocked
}
override suspend fun unlockAndSelect(selectedWalletId: UserWalletId): CompletionResult<UserWallet> {
return unlockWithBiometryInternal(selectedWalletId = selectedWalletId).mapUnlockResult()
val selectedUserWallet = selectedUserWalletSync
if (selectedUserWallet == null || selectedUserWallet.isLocked) {
Timber.e("Unable to find selected user wallet")
throw UserWalletsListError.NoUserWalletSelected
} else {
selectedUserWallet
}
}
}
override fun lock() {
@ -189,7 +205,7 @@ internal class BiometricUserWalletsListManager(
}
}
private suspend fun unlockWithBiometryInternal(selectedWalletId: UserWalletId? = null): CompletionResult<Unit> {
private suspend fun unlockWithBiometryInternal(): CompletionResult<Unit> {
return keysRepository.getAll()
.map { keys ->
state.update { prevState ->
@ -198,7 +214,7 @@ internal class BiometricUserWalletsListManager(
)
}
}
.flatMap { loadModels(selectedWalletId = selectedWalletId) }
.flatMap { loadModels() }
.map {
state.update { prevState ->
val hasLockedUserWallets = prevState.userWallets.any { it.isLocked }
@ -207,23 +223,6 @@ internal class BiometricUserWalletsListManager(
}
}
private fun CompletionResult<Unit>.mapUnlockResult(): CompletionResult<UserWallet> {
return this
.mapFailure { error ->
if (error is UserWalletsListError) {
error
} else {
UserWalletsListError.UnableToUnlockUserWallets(cause = error)
}
}
.map {
selectedUserWalletSync.guard {
Timber.e("Unable to find selected user wallet")
throw UserWalletsListError.NoUserWalletSelected
}
}
}
private suspend fun saveEncryptionKeyIfNotNull(userWallet: UserWallet): CompletionResult<ByteArray?> {
val encryptionKey = userWallet.scanResponse.card.encryptionKey
?.let { UserWalletEncryptionKey(userWallet.walletId, it) }
@ -245,7 +244,7 @@ internal class BiometricUserWalletsListManager(
}
}
private suspend fun loadModels(selectedWalletId: UserWalletId? = null): CompletionResult<Unit> {
private suspend fun loadModels(): CompletionResult<Unit> {
return getSavedUserWallets()
.map { userWallets ->
if (userWallets.isNotEmpty()) {
@ -255,7 +254,7 @@ internal class BiometricUserWalletsListManager(
prevState.copy(
userWallets = wallets,
selectedUserWalletId = findOrSetSelectedUserWalletId(
prevSelectedWalletId = selectedWalletId ?: prevState.selectedUserWalletId,
prevSelectedWalletId = prevState.selectedUserWalletId,
userWallets = wallets,
),
)
@ -316,7 +315,7 @@ internal class BiometricUserWalletsListManager(
private fun findSelectedUserWallet(userWallets: List<UserWallet> = state.value.userWallets): UserWallet? {
return userWallets.firstOrNull {
it.walletId == state.value.selectedUserWalletId && !it.isLocked
it.walletId == state.value.selectedUserWalletId
}
}

View file

@ -19,14 +19,6 @@ sealed class UserWalletsListError(code: Int) : TangemError(code) {
override var customMessage: String = "Encryption key invalidated"
}
object BiometricsAuthenticationDisabled : UserWalletsListError(code = 60005) {
override var customMessage: String = "Biometrics authentication disabled"
}
object NoUserWalletSelected : UserWalletsListError(code = 60006) {
override var customMessage: String = "No user wallet selected"
}
data class BiometricsAuthenticationLockout(val isPermanent: Boolean) : UserWalletsListError(code = 60003) {
override var customMessage: String = "Biometric authentication lockout, permanent: $isPermanent"
}
@ -35,4 +27,16 @@ sealed class UserWalletsListError(code: Int) : TangemError(code) {
override var customMessage: String = "An error has occurred, please scan your card to log in"
override val messageResId: Int = R.string.user_wallet_list_error_unable_to_unlock
}
object BiometricsAuthenticationDisabled : UserWalletsListError(code = 60005) {
override var customMessage: String = "Biometrics authentication disabled"
}
object NoUserWalletSelected : UserWalletsListError(code = 60006) {
override var customMessage: String = "No user wallet selected"
}
object NotAllUserWalletsUnlocked : UserWalletsListError(code = 60007) {
override var customMessage: String = "Not all user wallets was unlocked"
}
}

View file

@ -105,17 +105,13 @@ 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.
*
* @return [CompletionResult] of operation, with selected [UserWallet]
* or null if there is no selected [UserWallet]
*/
suspend fun unlock(): CompletionResult<UserWallet>
/**
* Unlock all [UserWallet]s and select passed [UserWalletId]
*
* @param selectedWalletId [UserWalletId] of [UserWallet] which must be selected
*/
suspend fun unlockAndSelect(selectedWalletId: UserWalletId): CompletionResult<UserWallet>
suspend fun unlock(throwIfNotAllWalletsUnlocked: Boolean = false): CompletionResult<UserWallet>
/** Remove [UserWallet]s from [userWallets] and set [isLocked] as true */
fun lock()

View file

@ -1,6 +0,0 @@
package com.tangem.domain.wallets.models
sealed interface UnlockWalletError {
object CommonError : UnlockWalletError
}

View file

@ -0,0 +1,12 @@
package com.tangem.domain.wallets.models
sealed class UnlockWalletsError {
object UnableToUnlockWallets : UnlockWalletsError()
object NoUserWalletSelected : UnlockWalletsError()
object NotAllUserWalletsUnlocked : UnlockWalletsError()
data class DataError(val cause: Throwable) : UnlockWalletsError()
}

View file

@ -1,14 +1,13 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import arrow.core.raise.either
import arrow.core.raise.ensureNotNull
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.wallets.models.UnlockWalletError
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.UnlockWalletsError
/**
* Unlock wallets use case
@ -19,14 +18,27 @@ import com.tangem.domain.wallets.models.UserWalletId
*/
class UnlockWalletsUseCase(private val walletsStateHolder: WalletsStateHolder) {
suspend operator fun invoke(selectedWalletId: UserWalletId): Either<UnlockWalletError, Unit> {
val userWalletsListManager = walletsStateHolder.userWalletsListManager?.asLockable()
?: return UnlockWalletError.CommonError.left()
suspend operator fun invoke(throwIfNotAllWalletsUnlocked: Boolean = false): Either<UnlockWalletsError, Unit> =
either {
val userWalletsListManager = ensureNotNull(
value = walletsStateHolder.userWalletsListManager?.asLockable(),
raise = {
UnlockWalletsError.DataError(
cause = IllegalStateException("The lockable user wallets list manager could not be found")
)
},
)
userWalletsListManager.unlockAndSelect(selectedWalletId = selectedWalletId)
.doOnSuccess { return Unit.right() }
.doOnFailure { return UnlockWalletError.CommonError.left() }
userWalletsListManager.unlock(throwIfNotAllWalletsUnlocked)
.doOnFailure { error ->
val e = when (error) {
is UserWalletsListError.NoUserWalletSelected -> UnlockWalletsError.NoUserWalletSelected
is UserWalletsListError.NotAllUserWalletsUnlocked ->
UnlockWalletsError.NotAllUserWalletsUnlocked
else -> UnlockWalletsError.UnableToUnlockWallets
}
return Unit.right()
}
raise(e)
}
}
}

View file

@ -32,4 +32,10 @@ internal sealed class WalletAlertState {
override val message: TextReference = resourceReference(R.string.error_wrong_wallet_tapped)
override val onConfirmClick: (() -> Unit)? = null
}
object RescanWallets : WalletAlertState() {
override val title: TextReference = resourceReference(R.string.common_attention)
override val message: TextReference = resourceReference(R.string.key_invalidated_warning_description)
override val onConfirmClick: (() -> Unit)? = null
}
}

View file

@ -57,6 +57,7 @@ import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.domain.walletconnect.WalletConnectActions
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UnlockWalletsError
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.*
@ -78,7 +79,6 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
import javax.inject.Inject
import kotlin.properties.Delegates
@ -511,10 +511,8 @@ internal class WalletViewModel @Inject constructor(
viewModelScope.launch(dispatchers.main) {
val userWallet = state.walletsListConfig.wallets[index]
withContext(dispatchers.io) {
if (userWallet !is WalletCardState.LockedContent) {
selectWalletUseCase(userWalletId = userWallet.id)
}
launch(dispatchers.io) {
selectWalletUseCase(userWallet.id)
}
val cacheState = WalletStateCache.getState(userWalletId = userWallet.id)
@ -842,17 +840,31 @@ internal class WalletViewModel @Inject constructor(
}
override fun onUnlockWalletClick() {
val state = uiState as? WalletState.ContentState ?: return
analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.NoticeWalletLocked)
viewModelScope.launch(dispatchers.main) {
unlockWalletsUseCase(
selectedWalletId = state.walletsListConfig.wallets[state.walletsListConfig.selectedWalletIndex].id,
)
unlockWalletsUseCase(throwIfNotAllWalletsUnlocked = true)
.onLeft(::handleUnlockWalletsError)
}
}
private fun handleUnlockWalletsError(error: UnlockWalletsError) {
val event = when (error) {
is UnlockWalletsError.DataError,
is UnlockWalletsError.UnableToUnlockWallets,
-> WalletEvent.ShowToast(resourceReference(R.string.user_wallet_list_error_unable_to_unlock))
is UnlockWalletsError.NoUserWalletSelected,
is UnlockWalletsError.NotAllUserWalletsUnlocked,
-> WalletEvent.ShowAlert(WalletAlertState.RescanWallets)
}
uiState = stateFactory.getStateAndTriggerEvent(
state = uiState,
event = event,
setUiState = { uiState = it },
)
}
override fun onUnlockWalletNotificationClick() {
val state = requireNotNull(uiState as? WalletLockedState) {
"Impossible to unlock wallet if state isn't WalletLockedState"