diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt index 199b92bf71..fe02ce769f 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt @@ -38,6 +38,7 @@ internal class BiometricUserWalletsListManager( override val userWalletsSync: List 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 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, 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.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 = state.value.userWallets, selectedUserWalletId: UserWalletId? = state.value.selectedUserWalletId, diff --git a/app/src/test/kotlin/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManagerTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManagerTest.kt new file mode 100644 index 0000000000..6d22031f4c --- /dev/null +++ b/app/src/test/kotlin/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManagerTest.kt @@ -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.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, + 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 { + 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 + } + }, + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index 77a46d4a0a..6bb90675a5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -153,7 +153,7 @@ internal object WalletScreenPreviewData { singleWalletLockedState, multiWalletState, ), - onWalletChange = {}, + onWalletChange = { _, _ -> }, event = consumedEvent(), isHidingMode = false, showMarketsOnboarding = false, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index 6dc50ed38f..6114dfeaf6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -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, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt index 1b61007fbb..ee8ed9bee9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt @@ -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() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt index ce541eaaa0..934382263b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt @@ -10,7 +10,7 @@ internal data class WalletScreenState( val topBarConfig: WalletTopBarConfig, val selectedWalletIndex: Int, val wallets: ImmutableList, - val onWalletChange: (Int) -> Unit, + val onWalletChange: (index: Int, onlyState: Boolean) -> Unit, val event: StateEvent, val isHidingMode: Boolean, val showMarketsOnboarding: Boolean, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt index 5fb52aca73..86e534351d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt @@ -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, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ScrollToWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ScrollToWalletTransformer.kt index d7135f0fdf..1bbd651f80 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ScrollToWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ScrollToWalletTransformer.kt @@ -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, 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() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt index f9cee1b0a5..a5e177dec1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt @@ -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, - 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)) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 828bee05c9..414d14cd71 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -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) + } + }, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletsListEffects.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletsListEffects.kt index d2173369f4..42e0916aff 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletsListEffects.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletsListEffects.kt @@ -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, - 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), ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt index 615036e995..7294996ccd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt @@ -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, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollector.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/CenterOfItemScrollingDetector.kt similarity index 61% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollector.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/CenterOfItemScrollingDetector.kt index 63fbab40fd..ecfac0d97a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollector.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/CenterOfItemScrollingDetector.kt @@ -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> { private val LazyListItemData.halfItemSize get() = size.div(other = 2) - private var currentIndex = selectedWalletIndex + private var currentIndex = initialIndex override suspend fun emit(value: List) { 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, -) \ No newline at end of file +internal data class LazyListItemData(val index: Int, val size: Int, val offset: Int) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index f06a3d46c4..cff354e8fa 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -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, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt index 2d675b4ca0..97073d4a82 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt @@ -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() } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt index 786be57b6c..b7b40ebe46 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt @@ -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,