Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-11 12:36:21 +03:00
parent d16abdc09b
commit 7fb7d4443a
3 changed files with 82 additions and 65 deletions

View file

@ -112,6 +112,7 @@ internal object UserWalletsListManagerModule {
publicInformationRepository = publicInformationRepository,
sensitiveInformationRepository = sensitiveInformationRepository,
selectedUserWalletRepository = selectedUserWalletRepository,
dispatcherProvider = dispatchers,
)
}

View file

@ -16,11 +16,13 @@ import com.tangem.tap.domain.userWalletList.utils.encryptionKey
import com.tangem.tap.domain.userWalletList.utils.lockAll
import com.tangem.tap.domain.userWalletList.utils.toUserWallets
import com.tangem.tap.domain.userWalletList.utils.updateWith
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import timber.log.Timber
@Suppress("LargeClass")
@ -30,6 +32,7 @@ internal class BiometricUserWalletsListManager(
private val publicInformationRepository: UserWalletsPublicInformationRepository,
private val sensitiveInformationRepository: UserWalletsSensitiveInformationRepository,
private val selectedUserWalletRepository: SelectedUserWalletRepository,
private val dispatcherProvider: CoroutineDispatcherProvider,
) : UserWalletsListManager.Lockable {
private val state = MutableStateFlow(State())
@ -72,12 +75,14 @@ internal class BiometricUserWalletsListManager(
return runBlocking {
// workaround to avoid calling hasSavedEncryptionKeys many times because of performance
savedWalletMutex.withLock {
Timber.i("Checking if user has saved wallets")
val hasSavedWalletsLocal = hasSavedWallets
if (hasSavedWalletsLocal == null || !hasSavedWalletsLocal) {
val hasKeys = keysRepository.hasSavedEncryptionKeys()
hasSavedWallets = hasKeys
hasKeys
} else {
Timber.i("User has saved wallets (from cache)")
true
}
}
@ -93,23 +98,25 @@ internal class BiometricUserWalletsListManager(
.distinctUntilChanged()
override suspend fun unlock(type: UnlockType): CompletionResult<UserWallet> {
return unlockAndSetSelectedUserWallet(type)
.mapFailure { error ->
Timber.e(error, "Unable to unlock user wallets")
if (error is UserWalletsListError) {
error
} else {
UserWalletsListError.UnableToUnlockUserWallets(error)
return withContext(dispatcherProvider.io) {
unlockAndSetSelectedUserWallet(type)
.mapFailure { error ->
Timber.e(error, "Unable to unlock user wallets")
if (error is UserWalletsListError) {
error
} else {
UserWalletsListError.UnableToUnlockUserWallets(error)
}
}
}
.map { selectedUserWallet ->
if (selectedUserWallet == null || selectedUserWallet.isLocked) {
Timber.e("Unable to find selected user wallet")
throw UserWalletsListError.NoUserWalletSelected
} else {
selectedUserWallet
.map { selectedUserWallet ->
if (selectedUserWallet == null || selectedUserWallet.isLocked) {
Timber.e("Unable to find selected user wallet")
throw UserWalletsListError.NoUserWalletSelected
} else {
selectedUserWallet
}
}
}
}
}
override fun lock() {
@ -141,18 +148,20 @@ internal class BiometricUserWalletsListManager(
}
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
return if (canOverride) {
saveInternal(userWallet, changeSelectedUserWallet = true, canOverridePublicInfo = false)
} else {
val isWalletSaved = state.value.userWallets
.any {
it.walletId == userWallet.walletId
}
if (isWalletSaved) {
CompletionResult.Failure(UserWalletsListError.WalletAlreadySaved)
} else {
return withContext(dispatcherProvider.io) {
if (canOverride) {
saveInternal(userWallet, changeSelectedUserWallet = true, canOverridePublicInfo = false)
} else {
val isWalletSaved = state.value.userWallets
.any {
it.walletId == userWallet.walletId
}
if (isWalletSaved) {
CompletionResult.Failure(UserWalletsListError.WalletAlreadySaved)
} else {
saveInternal(userWallet, changeSelectedUserWallet = true, canOverridePublicInfo = false)
}
}
}
}
@ -161,16 +170,18 @@ internal class BiometricUserWalletsListManager(
userWalletId: UserWalletId,
update: suspend (UserWallet) -> UserWallet,
): CompletionResult<UserWallet> {
return get(userWalletId)
.map { storedUserWallet ->
update(storedUserWallet)
}
.flatMap { updatedUserWallet ->
saveInternal(updatedUserWallet, changeSelectedUserWallet = false, canOverridePublicInfo = true)
}
.flatMap {
get(userWalletId)
}
return withContext(dispatcherProvider.io) {
get(userWalletId)
.map { storedUserWallet ->
update(storedUserWallet)
}
.flatMap { updatedUserWallet ->
saveInternal(updatedUserWallet, changeSelectedUserWallet = false, canOverridePublicInfo = true)
}
.flatMap {
get(userWalletId)
}
}
}
override suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit> {
@ -187,44 +198,48 @@ internal class BiometricUserWalletsListManager(
return clear()
}
return sensitiveInformationRepository.delete(idsToRemove)
.flatMap { publicInformationRepository.delete(idsToRemove) }
.map { keysRepository.delete(idsToRemove) }
.map {
state.update { prevState ->
val remainingWallets = prevState.userWallets.filter { it.walletId !in idsToRemove }
return withContext(dispatcherProvider.io) {
sensitiveInformationRepository.delete(idsToRemove)
.flatMap { publicInformationRepository.delete(idsToRemove) }
.map { keysRepository.delete(idsToRemove) }
.map {
state.update { prevState ->
val remainingWallets = prevState.userWallets.filter { it.walletId !in idsToRemove }
val isSelectedWalletDeleted = prevState.selectedUserWalletId in idsToRemove
val newSelectedUserWallet = findOrSetSelectedWallet(
prevSelectedWalletId = prevState.selectedUserWalletId,
prevSelectedWalletIndex = prevState.userWallets.indexOfFirst {
it.walletId == prevState.selectedUserWalletId
},
userWallets = remainingWallets,
ignorePrevSelectedWallet = isSelectedWalletDeleted,
)
val isSelectedWalletDeleted = prevState.selectedUserWalletId in idsToRemove
val newSelectedUserWallet = findOrSetSelectedWallet(
prevSelectedWalletId = prevState.selectedUserWalletId,
prevSelectedWalletIndex = prevState.userWallets.indexOfFirst {
it.walletId == prevState.selectedUserWalletId
},
userWallets = remainingWallets,
ignorePrevSelectedWallet = isSelectedWalletDeleted,
)
prevState.copy(
encryptionKeys = prevState.encryptionKeys.filter { it.walletId !in idsToRemove },
userWallets = remainingWallets,
isLocked = remainingWallets.any { it.isLocked },
selectedUserWalletId = newSelectedUserWallet?.walletId,
)
prevState.copy(
encryptionKeys = prevState.encryptionKeys.filter { it.walletId !in idsToRemove },
userWallets = remainingWallets,
isLocked = remainingWallets.any { it.isLocked },
selectedUserWalletId = newSelectedUserWallet?.walletId,
)
}
}
}
}
}
override suspend fun clear(): CompletionResult<Unit> {
savedWalletMutex.withLock {
hasSavedWallets = null
}
return sensitiveInformationRepository.clear()
.flatMap { publicInformationRepository.clear() }
.map {
keysRepository.clear()
selectedUserWalletRepository.set(null)
state.value = State()
}
return withContext(dispatcherProvider.io) {
sensitiveInformationRepository.clear()
.flatMap { publicInformationRepository.clear() }
.map {
keysRepository.clear()
selectedUserWalletRepository.set(null)
state.value = State()
}
}
}
override suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet> {