Updated on 2026-08-14
This commit is contained in:
parent
8cb8795f92
commit
dbdffaa974
16 changed files with 379 additions and 111 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -153,7 +153,7 @@ internal object WalletScreenPreviewData {
|
|||
singleWalletLockedState,
|
||||
multiWalletState,
|
||||
),
|
||||
onWalletChange = {},
|
||||
onWalletChange = { _, _ -> },
|
||||
event = consumedEvent(),
|
||||
isHidingMode = false,
|
||||
showMarketsOnboarding = false,
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ internal class WalletStateController @Inject constructor() {
|
|||
topBarConfig = WalletTopBarConfig(onDetailsClick = {}),
|
||||
selectedWalletIndex = NOT_INITIALIZED_WALLET_INDEX,
|
||||
wallets = persistentListOf(),
|
||||
onWalletChange = {},
|
||||
onWalletChange = { _, _ -> },
|
||||
event = consumedEvent(),
|
||||
isHidingMode = false,
|
||||
showMarketsOnboarding = false,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
@Immutable
|
||||
internal sealed class WalletEvent {
|
||||
|
||||
data class ChangeWallet(val index: Int) : WalletEvent()
|
||||
/** Change wallet with animation. Using [prevIndex] and [newIndex] to calculate offset */
|
||||
data class ChangeWallet(val prevIndex: Int, val newIndex: Int) : WalletEvent()
|
||||
|
||||
data class ShowError(val text: TextReference) : WalletEvent()
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ internal data class WalletScreenState(
|
|||
val topBarConfig: WalletTopBarConfig,
|
||||
val selectedWalletIndex: Int,
|
||||
val wallets: ImmutableList<WalletState>,
|
||||
val onWalletChange: (Int) -> Unit,
|
||||
val onWalletChange: (index: Int, onlyState: Boolean) -> Unit,
|
||||
val event: StateEvent<WalletEvent>,
|
||||
val isHidingMode: Boolean,
|
||||
val showMarketsOnboarding: Boolean,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ internal class AddWalletTransformer(
|
|||
private val clickIntents: WalletClickIntents,
|
||||
private val walletImageResolver: WalletImageResolver,
|
||||
private val walletFeatureToggles: WalletFeatureToggles,
|
||||
private val selectedWalletIndex: Int,
|
||||
) : WalletScreenStateTransformer {
|
||||
|
||||
private val walletLoadingStateFactory by lazy {
|
||||
|
|
@ -27,7 +26,6 @@ internal class AddWalletTransformer(
|
|||
override fun transform(prevState: WalletScreenState): WalletScreenState {
|
||||
return prevState.copy(
|
||||
wallets = (prevState.wallets + walletLoadingStateFactory.create(userWallet)).toImmutableList(),
|
||||
selectedWalletIndex = selectedWalletIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,8 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenSta
|
|||
import com.tangem.utils.Provider
|
||||
|
||||
internal class ScrollToWalletTransformer(
|
||||
private val index: Int,
|
||||
private val prevIndex: Int,
|
||||
private val newIndex: Int,
|
||||
private val currentStateProvider: Provider<WalletScreenState>,
|
||||
private val stateUpdater: (WalletScreenState) -> Unit,
|
||||
private val onConsume: () -> Unit = {},
|
||||
|
|
@ -16,13 +17,10 @@ internal class ScrollToWalletTransformer(
|
|||
override fun transform(prevState: WalletScreenState): WalletScreenState {
|
||||
return prevState.copy(
|
||||
event = triggeredEvent(
|
||||
data = WalletEvent.ChangeWallet(index),
|
||||
data = WalletEvent.ChangeWallet(prevIndex = prevIndex, newIndex = newIndex),
|
||||
onConsume = {
|
||||
stateUpdater(
|
||||
currentStateProvider().copy(
|
||||
selectedWalletIndex = index,
|
||||
event = consumedEvent(),
|
||||
),
|
||||
currentStateProvider().copy(event = consumedEvent()),
|
||||
)
|
||||
|
||||
onConsume()
|
||||
|
|
|
|||
|
|
@ -16,15 +16,12 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent
|
|||
import com.tangem.feature.wallet.presentation.wallet.ui.utils.ReviewManagerRequester
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.utils.animateScrollByIndex
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.utils.demonstrateScrolling
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
internal fun WalletEventEffect(
|
||||
walletsListState: LazyListState,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
event: StateEvent<WalletEvent>,
|
||||
selectedWalletIndex: Int,
|
||||
onAutoScrollSet: () -> Unit,
|
||||
onAlertConfigSet: (WalletAlertState) -> Unit,
|
||||
) {
|
||||
|
|
@ -36,9 +33,8 @@ internal fun WalletEventEffect(
|
|||
onTrigger = { value ->
|
||||
when (value) {
|
||||
is WalletEvent.ChangeWallet -> {
|
||||
delay(timeMillis = 1000) // to let compose time to start drawing otherwise animation doesnt work
|
||||
onAutoScrollSet()
|
||||
walletsListState.animateScrollByIndex(prevIndex = selectedWalletIndex, newIndex = value.index)
|
||||
walletsListState.animateScrollByIndex(prevIndex = value.prevIndex, newIndex = value.newIndex)
|
||||
}
|
||||
is WalletEvent.ShowError -> {
|
||||
snackbarHostState.showSnackbar(message = value.text.resolveReference(resources))
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBot
|
|||
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
|
||||
import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer
|
||||
import com.tangem.core.ui.components.rememberIsKeyboardVisible
|
||||
import com.tangem.core.ui.components.sheetscaffold.*
|
||||
import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar
|
||||
|
|
@ -58,7 +59,6 @@ import com.tangem.core.ui.components.snackbar.TangemSnackbar
|
|||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.LocalWindowSize
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -119,12 +119,11 @@ internal fun WalletScreen(state: WalletScreenState, marketsEntryComponent: Marke
|
|||
)
|
||||
|
||||
WalletEventEffect(
|
||||
event = state.event,
|
||||
selectedWalletIndex = state.selectedWalletIndex,
|
||||
walletsListState = walletsListState,
|
||||
snackbarHostState = snackbarHostState,
|
||||
onAlertConfigSet = { alertConfig = it },
|
||||
event = state.event,
|
||||
onAutoScrollSet = { isAutoScroll.value = true },
|
||||
onAlertConfigSet = { alertConfig = it },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -139,7 +138,11 @@ private fun WalletContent(
|
|||
alertConfig: WalletAlertState?,
|
||||
onAutoScrollReset: () -> Unit,
|
||||
) {
|
||||
var selectedWalletIndex by remember(state.selectedWalletIndex) { mutableIntStateOf(state.selectedWalletIndex) }
|
||||
/*
|
||||
* Don't pass key to remember, because it will brake scroll animation.
|
||||
* selectedWalletIndex will be changed in WalletsListEffects.
|
||||
*/
|
||||
val selectedWalletIndex by remember(state.selectedWalletIndex) { mutableIntStateOf(state.selectedWalletIndex) }
|
||||
val selectedWallet = state.wallets.getOrElse(selectedWalletIndex) { state.wallets[state.selectedWalletIndex] }
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
|
|
@ -251,10 +254,15 @@ private fun WalletContent(
|
|||
WalletsListEffects(
|
||||
lazyListState = walletsListState,
|
||||
selectedWalletIndex = selectedWalletIndex,
|
||||
onWalletChange = state.onWalletChange,
|
||||
onSelectedWalletIndexSet = { selectedWalletIndex = it },
|
||||
isAutoScroll = isAutoScroll,
|
||||
onAutoScrollReset = onAutoScrollReset,
|
||||
onUserScroll = onAutoScrollReset,
|
||||
onIndexChange = { index ->
|
||||
// Auto scroll must not change wallet
|
||||
if (isAutoScroll.value) {
|
||||
state.onWalletChange(index, true)
|
||||
} else {
|
||||
state.onWalletChange(index, false)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,48 +3,44 @@ package com.tangem.feature.wallet.presentation.wallet.ui
|
|||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.utils.CenterOfItemScrollingDetector
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.utils.LazyListItemData
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollector
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.utils.WalletsListInteractionsCollector
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
/**
|
||||
* Wallets list effects
|
||||
*
|
||||
* @param lazyListState wallets list [LazyListState]
|
||||
* @param selectedWalletIndex selected wallet index
|
||||
* @param onUserScroll lambda be invoked when user scrolls the list
|
||||
* @param onIndexChange lambda be invoked when index is changed
|
||||
*/
|
||||
@Composable
|
||||
internal fun WalletsListEffects(
|
||||
lazyListState: LazyListState,
|
||||
selectedWalletIndex: Int,
|
||||
onWalletChange: (Int) -> Unit,
|
||||
onSelectedWalletIndexSet: (Int) -> Unit,
|
||||
isAutoScroll: State<Boolean>,
|
||||
onAutoScrollReset: () -> Unit,
|
||||
onUserScroll: () -> Unit,
|
||||
onIndexChange: (Int) -> Unit,
|
||||
) {
|
||||
LaunchedEffect(key1 = lazyListState, key2 = onWalletChange) {
|
||||
LaunchedEffect(key1 = lazyListState) {
|
||||
snapshotFlow {
|
||||
lazyListState.layoutInfo.visibleItemsInfo.map {
|
||||
LazyListItemData(it.index, it.size, it.offset)
|
||||
}
|
||||
}
|
||||
.collect(
|
||||
collector = ScrollOffsetCollector(
|
||||
selectedWalletIndex = selectedWalletIndex,
|
||||
collector = CenterOfItemScrollingDetector(
|
||||
initialIndex = selectedWalletIndex,
|
||||
lazyListState = lazyListState,
|
||||
onWalletChange = { newIndex ->
|
||||
// Auto scroll must not change wallet
|
||||
if (isAutoScroll.value) {
|
||||
onSelectedWalletIndexSet(newIndex)
|
||||
} else {
|
||||
onSelectedWalletIndexSet(newIndex)
|
||||
onWalletChange(newIndex)
|
||||
}
|
||||
},
|
||||
onIndexChange = { newIndex -> onIndexChange(newIndex) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
lazyListState.interactionSource.interactions.collect(
|
||||
collector = WalletsListInteractionsCollector(onDragStart = onAutoScrollReset),
|
||||
collector = WalletsListInteractionsCollector(onDragStart = onUserScroll),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -58,7 +58,8 @@ internal fun WalletsList(
|
|||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
flingBehavior = rememberWalletsFlingBehaviour(lazyListState = lazyListState, itemWidth = itemWidth),
|
||||
) {
|
||||
items(items = wallets, contentType = { it.id.stringValue }) { state ->
|
||||
// Using key is required to guarantee the correct wallet selection
|
||||
items(items = wallets, key = { it.id.stringValue }, contentType = { "wallet_card" }) { state ->
|
||||
WalletCard(
|
||||
state = state,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
|
|
|
|||
|
|
@ -5,26 +5,26 @@ import kotlinx.coroutines.flow.FlowCollector
|
|||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* Flow collector for scroll items tracking.
|
||||
* Scrolling detector that changes the current element when scrolling the center of the element.
|
||||
* If first visible item offset is greater than half item size, then change selected wallet index.
|
||||
* If last visible item offset is greater than half item size, then change selected wallet index.
|
||||
*
|
||||
* @param selectedWalletIndex selected wallet index
|
||||
* @property lazyListState lazy list state
|
||||
* @property onWalletChange callback that will be invoked on wallet change
|
||||
* @param initialIndex initial index
|
||||
* @property lazyListState lazy list state
|
||||
* @property onIndexChange lambda be invoked when index is changed
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class ScrollOffsetCollector(
|
||||
selectedWalletIndex: Int,
|
||||
internal class CenterOfItemScrollingDetector(
|
||||
initialIndex: Int,
|
||||
private val lazyListState: LazyListState,
|
||||
private val onWalletChange: (Int) -> Unit,
|
||||
private val onIndexChange: (Int) -> Unit,
|
||||
) : FlowCollector<List<LazyListItemData>> {
|
||||
|
||||
private val LazyListItemData.halfItemSize
|
||||
get() = size.div(other = 2)
|
||||
|
||||
private var currentIndex = selectedWalletIndex
|
||||
private var currentIndex = initialIndex
|
||||
|
||||
override suspend fun emit(value: List<LazyListItemData>) {
|
||||
if (!lazyListState.isScrollInProgress || value.size <= 1) return
|
||||
|
|
@ -33,22 +33,18 @@ internal class ScrollOffsetCollector(
|
|||
val lastItem = value.lastOrNull() ?: return
|
||||
|
||||
if (abs(firstItem.offset) > firstItem.halfItemSize) {
|
||||
selectIndex(newIndex = firstItem.index + 1)
|
||||
changeIndex(newIndex = firstItem.index + 1)
|
||||
} else if (abs(lastItem.offset) > lastItem.halfItemSize) {
|
||||
selectIndex(newIndex = lastItem.index - 1)
|
||||
changeIndex(newIndex = lastItem.index - 1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectIndex(newIndex: Int) {
|
||||
private fun changeIndex(newIndex: Int) {
|
||||
if (currentIndex != newIndex) {
|
||||
currentIndex = newIndex
|
||||
onWalletChange(newIndex)
|
||||
onIndexChange(newIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal data class LazyListItemData(
|
||||
val index: Int,
|
||||
val size: Int,
|
||||
val offset: Int,
|
||||
)
|
||||
internal data class LazyListItemData(val index: Int, val size: Int, val offset: Int)
|
||||
|
|
@ -35,7 +35,6 @@ import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
|
|||
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.*
|
||||
import com.tangem.utils.extensions.indexOfFirstOrNull
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
|
@ -204,8 +203,6 @@ internal class WalletViewModel @Inject constructor(
|
|||
selectedWalletAnalyticsSender.send(selectedWallet)
|
||||
}
|
||||
|
||||
changeSelectedWalletState(selectedWalletId = selectedWallet.walletId)
|
||||
|
||||
walletDeepLinksHandler.registerForWallet(viewModel = this, userWallet = selectedWallet)
|
||||
}
|
||||
.flowOn(dispatchers.main)
|
||||
|
|
@ -213,22 +210,6 @@ internal class WalletViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
/** Change selected wallet state if selected wallet [selectedWalletId] was changed in the background */
|
||||
private suspend fun changeSelectedWalletState(selectedWalletId: UserWalletId) {
|
||||
if (!stateHolder.isInitialized) return
|
||||
|
||||
if (screenLifecycleProvider.isBackgroundState.value && selectedWalletId != stateHolder.getSelectedWalletId()) {
|
||||
stateHolder.value.wallets
|
||||
.indexOfFirstOrNull { prevState -> prevState.walletCardState.id == selectedWalletId }
|
||||
?.let { selectedIndex ->
|
||||
Timber.e("Selected wallet changed from background state: $selectedWalletId")
|
||||
|
||||
delay(timeMillis = 1000)
|
||||
scrollToWallet(selectedIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We need to update the current wallet quotes if the application was in the background for more than 10 seconds
|
||||
// and then returned to the foreground
|
||||
private fun subscribeToScreenBackgroundState() {
|
||||
|
|
@ -383,11 +364,14 @@ internal class WalletViewModel @Inject constructor(
|
|||
clickIntents = clickIntents,
|
||||
walletImageResolver = walletImageResolver,
|
||||
walletFeatureToggles = walletFeatureToggles,
|
||||
selectedWalletIndex = action.selectedWalletIndex,
|
||||
),
|
||||
)
|
||||
|
||||
scrollToWallet(index = action.selectedWalletIndex)
|
||||
scrollToWallet(prevIndex = action.prevWalletIndex, newIndex = action.selectedWalletIndex) {
|
||||
stateHolder.update {
|
||||
it.copy(selectedWalletIndex = action.selectedWalletIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun deleteWallet(action: WalletsUpdateActionResolver.Action.DeleteWallet) {
|
||||
|
|
@ -400,27 +384,42 @@ internal class WalletViewModel @Inject constructor(
|
|||
coroutineScope = viewModelScope,
|
||||
)
|
||||
|
||||
val newSelectedWalletIndex = if (action.selectedWalletIndex - action.deletedWalletIndex == 1) {
|
||||
action.deletedWalletIndex
|
||||
} else {
|
||||
action.selectedWalletIndex
|
||||
}
|
||||
|
||||
/*
|
||||
* Should not show scroll animation if WalletScreen isn't in the background.
|
||||
* Example, reset card
|
||||
*/
|
||||
if (screenLifecycleProvider.isBackgroundState.value) {
|
||||
updateStateByDeleteWalletTransformer(action)
|
||||
updateStateByDeleteWalletTransformer(
|
||||
selectedWalletIndex = newSelectedWalletIndex,
|
||||
deletedWalletId = action.deletedWalletId,
|
||||
)
|
||||
} else {
|
||||
withContext(dispatchers.io) { delay(timeMillis = 1000) }
|
||||
|
||||
scrollToWallet(
|
||||
index = action.selectedWalletIndex,
|
||||
onConsume = { updateStateByDeleteWalletTransformer(action) },
|
||||
prevIndex = action.deletedWalletIndex,
|
||||
newIndex = action.selectedWalletIndex,
|
||||
onConsume = {
|
||||
updateStateByDeleteWalletTransformer(
|
||||
selectedWalletIndex = newSelectedWalletIndex,
|
||||
deletedWalletId = action.deletedWalletId,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStateByDeleteWalletTransformer(action: WalletsUpdateActionResolver.Action.DeleteWallet) {
|
||||
private fun updateStateByDeleteWalletTransformer(selectedWalletIndex: Int, deletedWalletId: UserWalletId) {
|
||||
stateHolder.update(
|
||||
DeleteWalletTransformer(
|
||||
selectedWalletIndex = action.selectedWalletIndex,
|
||||
deletedWalletId = action.deletedWalletId,
|
||||
selectedWalletIndex = selectedWalletIndex,
|
||||
deletedWalletId = deletedWalletId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -444,10 +443,11 @@ internal class WalletViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun scrollToWallet(index: Int, onConsume: () -> Unit = {}) {
|
||||
private fun scrollToWallet(prevIndex: Int, newIndex: Int, onConsume: () -> Unit = {}) {
|
||||
stateHolder.update(
|
||||
ScrollToWalletTransformer(
|
||||
index = index,
|
||||
prevIndex = prevIndex,
|
||||
newIndex = newIndex,
|
||||
currentStateProvider = Provider(action = stateHolder::value),
|
||||
stateUpdater = { newState -> stateHolder.update { newState } },
|
||||
onConsume = onConsume,
|
||||
|
|
|
|||
|
|
@ -90,15 +90,19 @@ internal class WalletsUpdateActionResolver @Inject constructor(
|
|||
|
||||
return when {
|
||||
prevWalletsSize > wallets.size -> {
|
||||
val deletedWalletId = state.wallets.getDeletedWalletId(wallets)
|
||||
|
||||
Action.DeleteWallet(
|
||||
selectedWallet = selectedWallet,
|
||||
selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId),
|
||||
deletedWalletId = state.wallets.getDeletedWalletId(wallets),
|
||||
deletedWalletId = deletedWalletId,
|
||||
deletedWalletIndex = state.selectedWalletIndex,
|
||||
)
|
||||
}
|
||||
prevWalletsSize < wallets.size -> {
|
||||
val newUserWallet = state.wallets.getAddedWallet(wallets)
|
||||
Action.AddWallet(
|
||||
prevWalletIndex = state.selectedWalletIndex,
|
||||
selectedWalletIndex = wallets.indexOfWallet(id = newUserWallet.walletId),
|
||||
selectedWallet = newUserWallet,
|
||||
)
|
||||
|
|
@ -266,6 +270,7 @@ internal class WalletsUpdateActionResolver @Inject constructor(
|
|||
val selectedWallet: UserWallet,
|
||||
val selectedWalletIndex: Int,
|
||||
val deletedWalletId: UserWalletId,
|
||||
val deletedWalletIndex: Int,
|
||||
) : Action() {
|
||||
|
||||
override fun toString(): String {
|
||||
|
|
@ -273,19 +278,25 @@ internal class WalletsUpdateActionResolver @Inject constructor(
|
|||
DeleteWallet(
|
||||
selectedWallet = ${selectedWallet.walletId},
|
||||
selectedWalletIndex = $selectedWalletIndex,
|
||||
deletedWalletId = $deletedWalletId
|
||||
deletedWalletId = $deletedWalletId,
|
||||
deletedWalletIndex = $deletedWalletIndex,
|
||||
)
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
data class AddWallet(val selectedWalletIndex: Int, val selectedWallet: UserWallet) : Action() {
|
||||
data class AddWallet(
|
||||
val prevWalletIndex: Int,
|
||||
val selectedWalletIndex: Int,
|
||||
val selectedWallet: UserWallet,
|
||||
) : Action() {
|
||||
|
||||
override fun toString(): String {
|
||||
return """
|
||||
AddWallet(
|
||||
prevWalletIndex = $prevWalletIndex,
|
||||
selectedWalletIndex = $selectedWalletIndex,
|
||||
selectedWallet = ${selectedWallet.walletId}
|
||||
selectedWallet = ${selectedWallet.walletId},
|
||||
)
|
||||
""".trimIndent()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,9 +67,14 @@ internal class WalletClickIntents @Inject constructor(
|
|||
pushPermissionClickIntentsImplementor.initialize(router, coroutineScope)
|
||||
}
|
||||
|
||||
fun onWalletChange(index: Int) {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
launch(dispatchers.main) { neverToShowWalletsScrollPreview() }
|
||||
fun onWalletChange(index: Int, onlyState: Boolean) {
|
||||
if (onlyState) {
|
||||
stateHolder.update { it.copy(selectedWalletIndex = index) }
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
launch { neverToShowWalletsScrollPreview() }
|
||||
|
||||
val maybeUserWallet = selectWalletUseCase(
|
||||
userWalletId = stateHolder.value.wallets[index].walletCardState.id,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue