Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-27 15:02:37 +03:00
parent 8cb8795f92
commit dbdffaa974
16 changed files with 379 additions and 111 deletions

View file

@ -38,6 +38,7 @@ internal class BiometricUserWalletsListManager(
override val userWalletsSync: List<UserWallet>
get() = state.value.userWallets
@Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features")
override val selectedUserWallet: Flow<UserWallet>
get() = state
.mapLatest { state ->
@ -46,6 +47,7 @@ internal class BiometricUserWalletsListManager(
.filterNotNull()
.distinctUntilChanged()
@Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features")
override val selectedUserWalletSync: UserWallet?
get() = findSelectedUserWallet()
@ -166,6 +168,9 @@ internal class BiometricUserWalletsListManager(
val isSelectedWalletDeleted = prevState.selectedUserWalletId in idsToRemove
val newSelectedUserWallet = findOrSetSelectedWallet(
prevSelectedWalletId = prevState.selectedUserWalletId,
prevSelectedWalletIndex = prevState.userWallets.indexOfFirst {
it.walletId == prevState.selectedUserWalletId
},
userWallets = remainingWallets,
ignorePrevSelectedWallet = isSelectedWalletDeleted,
)
@ -248,27 +253,33 @@ internal class BiometricUserWalletsListManager(
throw UserWalletsListError.NotAllUserWalletsUnlocked
} else {
val prevState = state.value
val selectedWallet = findOrSetSelectedWallet(
state.value.selectedUserWalletId,
loadedState.userWallets,
prevSelectedWalletId = prevState.selectedUserWalletId,
userWallets = loadedState.userWallets,
prevSelectedWalletIndex = prevState.userWallets.indexOfFirst {
it.walletId == prevState.selectedUserWalletId
},
)
state.value = loadedState.copy(
selectedUserWalletId = selectedWallet?.walletId,
)
state.value = loadedState.copy(selectedUserWalletId = selectedWallet?.walletId)
selectedWallet
}
}
UnlockType.ANY -> {
val prevState = state.value
val selectedWallet = findOrSetSelectedWallet(
state.value.selectedUserWalletId,
loadedState.userWallets,
prevSelectedWalletId = state.value.selectedUserWalletId,
prevSelectedWalletIndex = prevState.userWallets.indexOfFirst {
it.walletId == prevState.selectedUserWalletId
},
userWallets = loadedState.userWallets,
)
state.value = loadedState.copy(
selectedUserWalletId = selectedWallet?.walletId,
)
state.value = loadedState.copy(selectedUserWalletId = selectedWallet?.walletId)
selectedWallet
}
@ -310,6 +321,7 @@ internal class BiometricUserWalletsListManager(
private fun findOrSetSelectedWallet(
prevSelectedWalletId: UserWalletId?,
prevSelectedWalletIndex: Int,
userWallets: List<UserWallet>,
ignorePrevSelectedWallet: Boolean = false,
): UserWallet? {
@ -321,7 +333,8 @@ internal class BiometricUserWalletsListManager(
}
if (possibleSelectedUserWallet == null || possibleSelectedUserWallet.isLocked) {
possibleSelectedUserWallet = userWallets.firstOrNull { !it.isLocked } ?: userWallets.firstOrNull()
possibleSelectedUserWallet =
userWallets.findAvailableUserWallet(prevSelectedIndex = prevSelectedWalletIndex)
}
selectedUserWalletRepository.set(possibleSelectedUserWallet?.walletId)
@ -329,6 +342,35 @@ internal class BiometricUserWalletsListManager(
return possibleSelectedUserWallet
}
/**
* Find the nearest available wallet that can be selected
*
* Example:
* Number with *n* is previous selected wallet with index [prevSelectedIndex].
*
* 1. [*1*, 2, 3, 4] => delete 1 => [2, 3, 4] => find and select => [*2*, 3, 4]
* 2. [1, *2*, 3, 4] => delete 2 => [1, 3, 4] => find and select => [1, *3*, 4]
* 3. [1, 2, *3*, 4] => delete 3 => [1, 2, 4] => find and select => [1, 2, *4*]
* 4. [1, 2, 3, *4*] => delete 4 => [1, 2, 3] => find and select => [1, 2, *3*]
*
* @receiver list of user wallets without deleted wallet
*/
private fun List<UserWallet>.findAvailableUserWallet(prevSelectedIndex: Int): UserWallet? {
if (prevSelectedIndex == 0) return firstOrNull { !it.isLocked } ?: firstOrNull()
if (prevSelectedIndex in indices && !this[prevSelectedIndex].isLocked) return this[prevSelectedIndex]
for (offset in 1..size) {
val rightIndex = prevSelectedIndex + offset
if (rightIndex in indices && !this[rightIndex].isLocked) return this[rightIndex]
val leftIndex = prevSelectedIndex - offset
if (leftIndex in indices && !this[leftIndex].isLocked) return this[leftIndex]
}
return lastOrNull()
}
private fun findSelectedUserWallet(
userWallets: List<UserWallet> = state.value.userWallets,
selectedUserWalletId: UserWalletId? = state.value.selectedUserWalletId,

View file

@ -0,0 +1,216 @@
package com.tangem.tap.domain.userWalletList.implementation
import com.google.common.truth.Truth
import com.tangem.domain.common.configs.GenericCardConfig
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.domain.card.ScanResponseMockFactory
import io.mockk.mockk
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import kotlin.reflect.full.declaredFunctions
import kotlin.reflect.jvm.isAccessible
/**
[REDACTED_AUTHOR]
*/
@RunWith(Parameterized::class)
internal class BiometricUserWalletsListManagerTest(private val model: Model) {
private val manager = BiometricUserWalletsListManager(
keysRepository = mockk(),
publicInformationRepository = mockk(),
sensitiveInformationRepository = mockk(),
selectedUserWalletRepository = mockk(),
)
@Test
fun testFindAvailableUserWallet() {
with(model) {
val actual = userWallets.findAvailableUserWallet(prevSelectedIndex)
Truth.assertThat(actual).isEqualTo(newSelectedWallet)
}
}
private fun List<UserWallet>.findAvailableUserWallet(prevSelectedIndex: Int): UserWallet? {
return manager::class
.declaredFunctions
.firstOrNull { it.name == "findAvailableUserWallet" }
?.apply { isAccessible = true }
?.call(manager, this, prevSelectedIndex)
as? UserWallet
}
data class Model(
val userWallets: List<UserWallet>,
val prevSelectedIndex: Int,
val newSelectedWallet: UserWallet?,
)
private companion object {
val userWallet0 = createUserWallet(id = "0", isLocked = false)
val lockedUserWallet0 = createUserWallet(id = "0", isLocked = true)
val userWallet1 = createUserWallet(id = "1", isLocked = false)
val lockedUserWallet1 = createUserWallet(id = "1", isLocked = true)
val userWallet2 = createUserWallet(id = "2", isLocked = false)
val lockedUserWallet2 = createUserWallet(id = "2", isLocked = true)
val userWallet3 = createUserWallet(id = "3", isLocked = false)
val lockedUserWallet3 = createUserWallet(id = "3", isLocked = true)
val unlockedWallets = listOf(userWallet0, userWallet1, userWallet2, userWallet3)
val lockedWallets = listOf(lockedUserWallet0, lockedUserWallet1, lockedUserWallet2, lockedUserWallet3)
@JvmStatic
@Parameterized.Parameters
fun data(): Collection<Model> {
return listOf(
Model(userWallets = emptyList(), prevSelectedIndex = 0, newSelectedWallet = null),
*getTestsWithUnlockedWallets().toTypedArray(),
*getTestsIfPrevSelectedIndexIs0().toTypedArray(),
*getTestsIfPrevSelectedIndexIsLastIndex().toTypedArray(),
*getTestsIfNewSelectedIndexIsNearby().toTypedArray(),
*getTestsIfNewSelectedIndexIsThroughOne().toTypedArray(),
)
}
fun getTestsWithUnlockedWallets() = listOf(
// [*0*, 1, 2, 3, 4] => delete 0 => [1, 2, 3, 4] => select 1 => [*1*, 2, 3, 4]
Model(userWallets = unlockedWallets, prevSelectedIndex = 0, newSelectedWallet = userWallet0),
// [0, *1*, 2, 3, 4] => delete 1 => [0, 2, 3, 4] => select 2 => [0, *2*, 3, 4]
Model(userWallets = unlockedWallets, prevSelectedIndex = 1, newSelectedWallet = userWallet1),
// [0, 1, *2*, 3, 4] => delete 2 => [0, 1, 3, 4] => select 3 => [0, 1, *3*, 4]
Model(userWallets = unlockedWallets, prevSelectedIndex = 2, newSelectedWallet = userWallet2),
// [0, 1, 2, *3*, 4] => delete 3 => [0, 1, 2, 4] => select 4 => [0, 1, 2, 4]
Model(userWallets = unlockedWallets, prevSelectedIndex = 3, newSelectedWallet = userWallet3),
// [0, 1, 2, 3, *4*] => delete 4 => [0, 1, 2, 3] => select 3 => [0, 1, 2, *3*]
Model(userWallets = unlockedWallets, prevSelectedIndex = 4, newSelectedWallet = userWallet3),
)
fun getTestsIfPrevSelectedIndexIs0() = listOf(
// [*0*, -1-, 2, 3, 4] => delete 0 => [-1-, 2, 3, 4] => select 2 => [-1-, *2*, 3, 4]
Model(
userWallets = listOf(lockedUserWallet0, userWallet1, userWallet2, userWallet3),
prevSelectedIndex = 0,
newSelectedWallet = userWallet1,
),
// [*0*, -1-, -2-, 3, 4] => delete 0 => [-1-, -2-, 3, 4] => select 3 => [-1-, -2-, *3*, 4]
Model(
userWallets = listOf(lockedUserWallet0, lockedUserWallet1, userWallet2, userWallet3),
prevSelectedIndex = 0,
newSelectedWallet = userWallet2,
),
// [*0*, -1-, -2-, -3-, 4] => delete 0 => [-1-, -2-, -3-, 4] => select 4 => [-1-, -2-, -3-, *4*]
Model(
userWallets = listOf(lockedUserWallet0, lockedUserWallet1, lockedUserWallet2, userWallet3),
prevSelectedIndex = 0,
newSelectedWallet = userWallet3,
),
// [*0*, -1-, -2-, -3-, -4-] => delete 0 => [-1-, -2-, -3-, -4-] => select 1 => [*-1-*, -2-, -3-, -4-]
Model(userWallets = lockedWallets, prevSelectedIndex = 0, newSelectedWallet = lockedUserWallet0),
)
fun getTestsIfPrevSelectedIndexIsLastIndex() = listOf(
// [0, 1, 2, -3-, *4*] => delete 4 => [0, 1, 2, -3-] => select 2 => [0, 1, *2*, -3-]
Model(
userWallets = listOf(userWallet0, userWallet1, userWallet2, lockedUserWallet3),
prevSelectedIndex = 4,
newSelectedWallet = userWallet2,
),
// [0, 1, -2-, -3-, *4*] => delete 4 => [0, 1, -2-, -3-] => select 1 => [0, *1*, -2-, -3-]
Model(
userWallets = listOf(userWallet0, userWallet1, lockedUserWallet2, lockedUserWallet3),
prevSelectedIndex = 4,
newSelectedWallet = userWallet1,
),
// [0, -1-, -2-, -3-, *4*] => delete 4 => [0, -1-, -2-, -3-] => select 0 => [*0*, -1-, -2-, -3-]
Model(
userWallets = listOf(userWallet0, lockedUserWallet1, lockedUserWallet2, lockedUserWallet3),
prevSelectedIndex = 4,
newSelectedWallet = userWallet0,
),
// [-0-, -1-, -2-, -3-, *4*] => delete 4 => [-0-, -1-, -2-, -3-] => select 3 => [-0-, -1-, -2-, *-3-*]
Model(userWallets = lockedWallets, prevSelectedIndex = 4, newSelectedWallet = lockedUserWallet3),
)
fun getTestsIfNewSelectedIndexIsNearby() = listOf(
// [0, *1*, 2, 3, 4] => delete 1 => [0, 2, 3, 4] => select 2 => [0, *2*, 3, 4]
Model(userWallets = unlockedWallets, prevSelectedIndex = 1, newSelectedWallet = userWallet1),
// [0, *1*, -2-, 3, 4] => delete 1 => [0, -2-, 3, 4] => select 3 => [0, -2-, *3*, 4]
Model(
userWallets = listOf(userWallet0, lockedUserWallet1, userWallet2, userWallet3),
prevSelectedIndex = 1,
newSelectedWallet = userWallet2,
),
// [0, *1*, -2-, -3-, 4] => delete 1 => [0, -2-, -3-, 4] => select 0 => [*0*, -2-, -3-, 4]
Model(
userWallets = listOf(userWallet0, lockedUserWallet1, lockedUserWallet2, userWallet3),
prevSelectedIndex = 1,
newSelectedWallet = userWallet0,
),
// [0, 1, 2, *3*, 4] => delete 3 => [0, 1, 2, 4] => select 4 => [0, 1, 2, *4*]
Model(userWallets = unlockedWallets, prevSelectedIndex = 3, newSelectedWallet = userWallet3),
// [0, 1, 2, *3*, -4-] => delete 3 => [0, 1, 2, -4-] => select 2 => [0, 1, *2*, -4-]
Model(
userWallets = listOf(userWallet0, userWallet1, userWallet2, lockedUserWallet3),
prevSelectedIndex = 3,
newSelectedWallet = userWallet2,
),
)
fun getTestsIfNewSelectedIndexIsThroughOne() = listOf(
// [-0-, *1*, -2-, 3, 4] => delete 1 => [-0-, -2-, 3, 4] => select 3 => [-0-, -2-, *3*, 4]
Model(
userWallets = listOf(lockedUserWallet0, lockedUserWallet1, userWallet2, userWallet3),
prevSelectedIndex = 1,
newSelectedWallet = userWallet2,
),
// [-0-, *1*, -2-, -3-, 4] => delete 1 => [-0-, -2-, -3-, 4] => select 4 => [-0-, -2-, -3-, *4*]
Model(
userWallets = listOf(lockedUserWallet0, lockedUserWallet1, lockedUserWallet2, userWallet3),
prevSelectedIndex = 1,
newSelectedWallet = userWallet3,
),
// [0, 1, -2-, *3*, -4-] => delete 3 => [0, 1, -2-, -4-] => select 1 => [0, *1*, -2-, -4-]
Model(
userWallets = listOf(userWallet0, userWallet1, lockedUserWallet2, lockedUserWallet3),
prevSelectedIndex = 3,
newSelectedWallet = userWallet1,
),
// [0, -1-, -2-, *3*, -4-] => delete 3 => [*0*, -1-, -2-, -4-] => select 0 => [0, -1-, -2-, -4-]
Model(
userWallets = listOf(userWallet0, lockedUserWallet1, lockedUserWallet2, lockedUserWallet3),
prevSelectedIndex = 3,
newSelectedWallet = userWallet0,
),
)
fun createUserWallet(id: String, isLocked: Boolean): UserWallet {
return UserWallet(
name = "Wallet $id",
walletId = UserWalletId(stringValue = id),
artworkUrl = "",
cardsInWallet = emptySet(),
isMultiCurrency = true,
hasBackupError = false,
scanResponse = ScanResponseMockFactory.create(
cardConfig = GenericCardConfig(maxWalletCount = 1),
derivedKeys = emptyMap(),
).let {
if (isLocked) {
it.copy(
card = it.card.copy(wallets = emptyList()),
)
} else {
it
}
},
)
}
}
}