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
}
},
)
}
}
}

View file

@ -153,7 +153,7 @@ internal object WalletScreenPreviewData {
singleWalletLockedState,
multiWalletState,
),
onWalletChange = {},
onWalletChange = { _, _ -> },
event = consumedEvent(),
isHidingMode = false,
showMarketsOnboarding = false,

View file

@ -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,

View file

@ -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()

View file

@ -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,

View file

@ -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,
)
}
}

View file

@ -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()

View file

@ -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))

View file

@ -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)
}
},
)
}

View file

@ -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),
)
}
}

View file

@ -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,

View file

@ -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)

View file

@ -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,

View file

@ -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()
}

View file

@ -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,