Updated on 2026-08-14
This commit is contained in:
parent
85c6044602
commit
d470f278fc
8 changed files with 108 additions and 73 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
package com.tangem.domain.wallets.models
|
||||
|
||||
sealed interface UnlockWalletError {
|
||||
|
||||
object CommonError : UnlockWalletError
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue