diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index 5ac3840c7f..b8861a9533 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -13,6 +13,7 @@ import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace @@ -68,25 +69,29 @@ internal class DefaultNetworksRepository( networks: Set, refresh: Boolean, ) { - cacheRegistry.invokeOnExpire( - key = getNetworksStatusesCacheKey(userWalletId), - skipCache = refresh, - block = { fetchNetworksStatuses(userWalletId, networks) }, - ) - } - - private suspend fun fetchNetworksStatuses(userWalletId: UserWalletId, networks: Set) { coroutineScope { networks .map { networkId -> async { - fetchNetworkStatus(userWalletId, networkId) + fetchNetworkStatusIfCacheExpired(userWalletId, networkId, refresh) } } .awaitAll() } } + private suspend fun fetchNetworkStatusIfCacheExpired( + userWalletId: UserWalletId, + networkId: Network.ID, + refresh: Boolean, + ) { + cacheRegistry.invokeOnExpire( + key = getNetworksStatusesCacheKey(userWalletId, networkId), + skipCache = refresh, + block = { fetchNetworkStatus(userWalletId, networkId) }, + ) + } + private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, networkId: Network.ID) { val currencies = getCurrencies(userWalletId) .asSequence() @@ -97,6 +102,17 @@ internal class DefaultNetworksRepository( networkId = networkId, extraTokens = currencies.filterIsInstance().toSet(), ) + + // Invalidate cache key if wallet manager update failed + when (result) { + is UpdateWalletManagerResult.Verified, + is UpdateWalletManagerResult.NoAccount, + -> Unit + is UpdateWalletManagerResult.Unreachable, + is UpdateWalletManagerResult.MissedDerivation, + -> cacheRegistry.invalidate(getNetworksStatusesCacheKey(userWalletId, networkId)) + } + val networkStatus = networkStatusFactory.createNetworkStatus( networkId = networkId, result = result, @@ -126,5 +142,7 @@ internal class DefaultNetworksRepository( } } - private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId): String = "network_status_$userWalletId" + private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId, nerworkId: Network.ID): String { + return "network_status_${userWalletId}_${nerworkId.value}" + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt index 89d6fe1405..b56de58375 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt @@ -8,7 +8,8 @@ import com.tangem.domain.wallets.models.GetSelectedWalletError import com.tangem.domain.wallets.models.UserWallet /** - * Use case for getting selected wallet + * Use case for getting selected wallet. + * Important! If all wallets is locked, use case returns a error. * * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' * diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletDeleteStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletDeleteStateConverter.kt new file mode 100644 index 0000000000..c6ccfcd05d --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletDeleteStateConverter.kt @@ -0,0 +1,50 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import com.tangem.common.Provider +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletDeleteStateConverter.DeleteWalletModel +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletsUpdateActionResolver +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +/** + * Converter that responds on wallet deleting action. Returns [WalletState] without deleted wallet. + * + * @property currentStateProvider current state provider + */ +internal class WalletDeleteStateConverter( + private val currentStateProvider: Provider, +) : Converter { + + override fun convert(value: DeleteWalletModel): WalletState { + return when (val state = currentStateProvider()) { + is WalletState.ContentState -> { + value.cacheState.copySealed( + walletsListConfig = state.walletsListConfig.copy( + selectedWalletIndex = value.action.selectedWalletIndex, + wallets = state.walletsListConfig.wallets.deleteWallet(id = value.action.deletedWalletId), + ), + pullToRefreshConfig = value.cacheState.pullToRefreshConfig.copy(isRefreshing = false), + ) + } + is WalletState.Initial -> state + } + } + + private fun List.deleteWallet(id: UserWalletId): ImmutableList { + return this + .mapIndexedNotNull { index, currentWallet -> + if (currentWallet.id == id) return@mapIndexedNotNull null + getOrNull(index) ?: return@mapIndexedNotNull null + } + .toImmutableList() + } + + data class DeleteWalletModel( + val cacheState: WalletState.ContentState, + val action: WalletsUpdateActionResolver.Action.DeleteWallet, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt index 3e89437610..79b2d32206 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt @@ -1,110 +1,78 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory import com.tangem.common.Provider -import com.tangem.domain.common.CardTypesResolver -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletPullToRefreshConfig import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopBarConfig -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList internal class WalletLockedConverter( private val currentStateProvider: Provider, - private val currentCardTypeResolverProvider: Provider, - private val currentWalletProvider: Provider, private val clickIntents: WalletClickIntents, ) : Converter { override fun convert(value: Unit): WalletState { return when (val state = currentStateProvider()) { - is WalletState.ContentState -> { - val cardTypeResolver = currentCardTypeResolverProvider() - - if (cardTypeResolver.isMultiwalletAllowed()) { - state.toMultiCurrencyLockedState(cardTypeResolver) - } else { - state.toSingleCurrencyLockedState(cardTypeResolver) - } - } - is WalletState.Initial -> state + is WalletMultiCurrencyState.Content -> state.toMultiCurrencyLockedState() + is WalletSingleCurrencyState.Content -> state.toSingleCurrencyLockedState() + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Locked, + is WalletState.Initial, + -> state } } - private fun WalletState.ContentState.toMultiCurrencyLockedState( - cardTypeResolver: CardTypesResolver, - ): WalletMultiCurrencyState.Locked { + private fun WalletMultiCurrencyState.Content.toMultiCurrencyLockedState(): WalletState { return WalletMultiCurrencyState.Locked( onBackClick = onBackClick, - topBarConfig = createTopBarConfig(), - walletsListConfig = createWalletsListConfig(cardTypeResolver), - pullToRefreshConfig = pullToRefreshConfig, + topBarConfig = topBarConfig.updateCallback(), + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig.stopRefreshing(), onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick, onUnlockClick = clickIntents::onUnlockWalletClick, - onScanClick = clickIntents::onScanCardClick, + onScanClick = clickIntents::onScanToUnlockWalletClick, ) } - private fun WalletState.ContentState.toSingleCurrencyLockedState( - cardTypeResolver: CardTypesResolver, - ): WalletSingleCurrencyState.Locked { + private fun WalletSingleCurrencyState.Content.toSingleCurrencyLockedState(): WalletState { return WalletSingleCurrencyState.Locked( onBackClick = onBackClick, - topBarConfig = createTopBarConfig(), - walletsListConfig = createWalletsListConfig(cardTypeResolver), - pullToRefreshConfig = pullToRefreshConfig, - buttons = createButtons(), + topBarConfig = topBarConfig.updateCallback(), + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig.stopRefreshing(), + buttons = buttons.disableButtons(), onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick, onUnlockClick = clickIntents::onUnlockWalletClick, - onScanClick = clickIntents::onScanCardClick, + onScanClick = clickIntents::onScanToUnlockWalletClick, onExploreClick = clickIntents::onExploreClick, ) } - private fun WalletState.ContentState.createTopBarConfig(): WalletTopBarConfig { - return topBarConfig.copy(onMoreClick = clickIntents::onUnlockWalletNotificationClick) + private fun WalletTopBarConfig.updateCallback(): WalletTopBarConfig { + return copy(onMoreClick = clickIntents::onUnlockWalletNotificationClick) } - private fun WalletState.ContentState.createWalletsListConfig( - cardTypeResolver: CardTypesResolver, - ): WalletsListConfig { - return walletsListConfig.copy( - wallets = walletsListConfig.wallets - .map { walletCardState -> - WalletCardState.LockedContent( - id = walletCardState.id, - title = walletCardState.title, - additionalInfo = if (cardTypeResolver.isMultiwalletAllowed()) { - WalletAdditionalInfoFactory.resolve( - cardTypesResolver = cardTypeResolver, - wallet = currentWalletProvider(), - ) - } else { - null - }, - imageResId = walletCardState.imageResId, - onRenameClick = walletCardState.onRenameClick, - onDeleteClick = walletCardState.onDeleteClick, - ) + private fun WalletPullToRefreshConfig.stopRefreshing(): WalletPullToRefreshConfig { + return copy(isRefreshing = false) + } + + private fun PersistentList.disableButtons(): PersistentList { + return this + .map { button -> + when (button) { + is WalletManageButton.Buy -> button.copy(enabled = false) + is WalletManageButton.Sell -> button.copy(enabled = false) + is WalletManageButton.Send -> button.copy(enabled = false) + is WalletManageButton.Swap -> button.copy(enabled = false) + is WalletManageButton.Receive -> button } - .toImmutableList(), - ) - } - - private fun createButtons(): PersistentList { - return persistentListOf( - WalletManageButton.Buy(enabled = false, onClick = {}), - WalletManageButton.Send(enabled = false, onClick = {}), - WalletManageButton.Receive(onClick = {}), - WalletManageButton.Sell(enabled = false, onClick = {}), - ) + } + .toPersistentList() } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRenameStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRenameStateConverter.kt new file mode 100644 index 0000000000..946afa55df --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRenameStateConverter.kt @@ -0,0 +1,33 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import com.tangem.common.Provider +import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toImmutableList + +internal class WalletRenameStateConverter( + private val currentStateProvider: Provider, +) : Converter { + + override fun convert(value: String): WalletState { + return when (val state = currentStateProvider()) { + is WalletState.ContentState -> { + state.copySealed( + walletsListConfig = state.walletsListConfig.renameSelectedWallet(name = value), + ) + } + is WalletState.Initial -> state + } + } + + private fun WalletsListConfig.renameSelectedWallet(name: String): WalletsListConfig { + return copy( + wallets = wallets + .mapIndexed { index, walletCard -> + if (index == selectedWalletIndex) walletCard.copySealed(title = name) else walletCard + } + .toImmutableList(), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt index 18892dce3f..b67b7cedb8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt @@ -11,6 +11,7 @@ import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState @@ -26,22 +27,31 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( private val appCurrencyProvider: Provider, private val currentWalletProvider: Provider, private val currencyStatusErrorConverter: CurrencyStatusErrorConverter, -) : Converter, WalletSingleCurrencyState.Content> { +) : Converter, WalletState> { - override fun convert(value: Either): WalletSingleCurrencyState.Content { + override fun convert(value: Either): WalletState { return value.fold( ifLeft = currencyStatusErrorConverter::convert, ifRight = ::convertContent, ) } - private fun convertContent(status: CryptoCurrencyStatus): WalletSingleCurrencyState.Content { - val state = requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content) - val currencyName = state.marketPriceBlockState.currencyName - return state.copy( - walletsListConfig = getUpdatedSelectedWallet(status = status.value, state = state), - marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName), - ) + private fun convertContent(status: CryptoCurrencyStatus): WalletState { + return when (val state = currentStateProvider()) { + is WalletSingleCurrencyState.Content -> { + val currencyName = state.marketPriceBlockState.currencyName + + state.copy( + walletsListConfig = getUpdatedSelectedWallet(status = status.value, state = state), + marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName), + ) + } + is WalletMultiCurrencyState.Content, + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Locked, + is WalletState.Initial, + -> state + } } private fun getMarketPriceState(status: CryptoCurrencyStatus.Status, currencyName: String): MarketPriceBlockState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt index 05b16b6424..166a46f8fa 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt @@ -1,8 +1,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory +import androidx.annotation.DrawableRes import com.tangem.common.Provider import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory @@ -33,12 +35,12 @@ internal class WalletSkeletonStateConverter( ) : Converter { override fun convert(value: SkeletonModel): WalletState.ContentState { - val cardTypeResolver = value.wallets[value.selectedWalletIndex].scanResponse.cardTypesResolver + val selectedWallet = value.wallets[value.selectedWalletIndex] - return if (cardTypeResolver.isMultiwalletAllowed()) { + return if (selectedWallet.isMultiCurrency) { createMultiCurrencyState(value = value) } else { - createSingleCurrencyState(value = value, currencyName = cardTypeResolver.getBlockchain().currency) + createSingleCurrencyState(value = value, currencyName = selectedWallet.getPrimaryCurrencyName()) } } @@ -74,6 +76,10 @@ internal class WalletSkeletonStateConverter( ) } + private fun UserWallet.getPrimaryCurrencyName(): String { + return scanResponse.cardTypesResolver.getBlockchain().currency + } + private fun createTopBarConfig(): WalletTopBarConfig { return WalletTopBarConfig( onScanCardClick = clickIntents::onScanCardClick, @@ -84,46 +90,63 @@ internal class WalletSkeletonStateConverter( private fun createWalletsListConfig(value: SkeletonModel): WalletsListConfig { return WalletsListConfig( selectedWalletIndex = value.selectedWalletIndex, - wallets = value.wallets.map(::createWalletState).toImmutableList(), + wallets = value.wallets.mapIndexed(::createWalletCardState).toImmutableList(), onWalletChange = clickIntents::onWalletChange, ) } - private fun createWalletState(wallet: UserWallet): WalletCardState { - val state = currentStateProvider() - - // If it isn't first initialization (example, when user unlocks wallet) - return if (state is WalletState.ContentState) { - val initializedWallet = state.walletsListConfig.wallets.first { it.id == wallet.walletId } - - // If wallet is initialized, return it, otherwise return loading state - if (initializedWallet !is WalletCardState.Loading) { - initializedWallet.copySealed(title = wallet.name) - } else { - createWalletLoadingState(wallet) - } - } else { - createWalletLoadingState(wallet) - } + /** + * Create wallet card state by [index] and [wallet]. + * If current wallet card state is initialized, then method returns it. + * Otherwise, returns loading wallet card state. + */ + private fun createWalletCardState(index: Int, wallet: UserWallet): WalletCardState { + return currentStateProvider().getInitializedWalletCardState(index) ?: wallet.mapToWalletCardState() } - private fun createWalletLoadingState(wallet: UserWallet): WalletCardState { - val cardTypeResolver = wallet.scanResponse.cardTypesResolver + private fun WalletState.getInitializedWalletCardState(index: Int): WalletCardState? { + return (this as? WalletState.ContentState)?.walletsListConfig?.wallets?.getOrNull(index) + } - return WalletCardState.Loading( - id = wallet.walletId, - title = wallet.name, - additionalInfo = if (cardTypeResolver.isMultiwalletAllowed()) { - WalletAdditionalInfoFactory.resolve(cardTypesResolver = cardTypeResolver, wallet = wallet) - } else { - null - }, - imageResId = WalletImageResolver.resolve(cardTypesResolver = cardTypeResolver), + private fun UserWallet.mapToWalletCardState(): WalletCardState { + return if (isLocked) mapToLockedWalletCardState() else mapToLoadingWalletCardState() + } + + private fun UserWallet.mapToLockedWalletCardState(): WalletCardState { + return WalletCardState.LockedContent( + id = walletId, + title = name, + additionalInfo = createAdditionalInfo(), + imageResId = createImageResId(), onRenameClick = clickIntents::onRenameClick, onDeleteClick = clickIntents::onDeleteClick, ) } + private fun UserWallet.mapToLoadingWalletCardState(): WalletCardState { + return WalletCardState.Loading( + id = walletId, + title = name, + additionalInfo = createAdditionalInfo(), + imageResId = createImageResId(), + onRenameClick = clickIntents::onRenameClick, + onDeleteClick = clickIntents::onDeleteClick, + ) + } + + private fun UserWallet.createAdditionalInfo(): TextReference? { + return if (isMultiCurrency) { + WalletAdditionalInfoFactory.resolve(cardTypesResolver = scanResponse.cardTypesResolver, wallet = this) + } else { + null + } + } + + @DrawableRes + private fun UserWallet.createImageResId(): Int? { + return WalletImageResolver.resolve(cardTypesResolver = scanResponse.cardTypesResolver) + } + private fun createPullToRefreshConfig(): WalletPullToRefreshConfig { return WalletPullToRefreshConfig(isRefreshing = false, onRefresh = clickIntents::onRefreshSwipe) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt index fa22261f52..ad2916e9b1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt @@ -25,6 +25,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.Wal import com.tangem.feature.wallet.presentation.wallet.utils.CurrencyStatusErrorConverter import com.tangem.feature.wallet.presentation.wallet.utils.TokenListErrorConverter import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletsUpdateActionResolver import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.Flow @@ -45,8 +46,15 @@ internal class WalletStateFactory( ) { private val tokenActionsProvider by lazy { TokenActionsProvider(clickIntents) } + private val skeletonConverter by lazy { WalletSkeletonStateConverter(currentStateProvider, clickIntents) } + private val walletsUnlockStateConverter by lazy { WalletsUnlockStateConverter(currentStateProvider, clickIntents) } + + private val walletRenameStateConverter by lazy { WalletRenameStateConverter(currentStateProvider) } + + private val walletDeleteStateConverter by lazy { WalletDeleteStateConverter(currentStateProvider) } + private val tokenListErrorConverter by lazy { TokenListErrorConverter(currentStateProvider) } @@ -92,8 +100,6 @@ internal class WalletStateFactory( private val lockedConverter by lazy { WalletLockedConverter( currentStateProvider = currentStateProvider, - currentCardTypeResolverProvider = currentCardTypeResolverProvider, - currentWalletProvider = currentWalletProvider, clickIntents = clickIntents, ) } @@ -123,6 +129,21 @@ internal class WalletStateFactory( ) } + fun getStateWithUpdatedWalletName(name: String): WalletState = walletRenameStateConverter.convert(value = name) + + fun getUnlockedState(action: WalletsUpdateActionResolver.Action.UnlockWallet): WalletState { + return walletsUnlockStateConverter.convert(value = action) + } + + fun getStateWithoutDeletedWallet( + cacheState: WalletState.ContentState, + action: WalletsUpdateActionResolver.Action.DeleteWallet, + ): WalletState { + return walletDeleteStateConverter.convert( + value = WalletDeleteStateConverter.DeleteWalletModel(cacheState = cacheState, action = action), + ) + } + fun getStateByTokensList(maybeTokenList: Either): WalletState { return loadedTokensListConverter.convert(maybeTokenList) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletsUnlockStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletsUnlockStateConverter.kt new file mode 100644 index 0000000000..cb9d0d7e7e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletsUnlockStateConverter.kt @@ -0,0 +1,142 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import com.tangem.common.Provider +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver +import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.components.* +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletsUpdateActionResolver.Action.UnlockWallet as UnlockWalletAction + +/** + * Converter that responds on wallets unlocking action. Returns [WalletState] with unlocked wallets. + * + * @property currentStateProvider current ui state provider + * @property clickIntents screen click intents + * +[REDACTED_AUTHOR] + */ +internal class WalletsUnlockStateConverter( + private val currentStateProvider: Provider, + private val clickIntents: WalletClickIntents, +) : Converter { + + override fun convert(value: UnlockWalletAction): WalletState { + return when (val state = currentStateProvider()) { + is WalletMultiCurrencyState.Locked -> state.toMultiCurrencyContentState(value) + is WalletSingleCurrencyState.Locked -> state.toSingleCurrencyContentState(value) + is WalletState.Initial, + is WalletMultiCurrencyState.Content, + is WalletSingleCurrencyState.Content, + -> state + } + } + + private fun WalletMultiCurrencyState.Locked.toMultiCurrencyContentState(action: UnlockWalletAction): WalletState { + return WalletMultiCurrencyState.Content( + onBackClick = onBackClick, + topBarConfig = topBarConfig.updateCallback(), + walletsListConfig = walletsListConfig.unlockWallets(action), + pullToRefreshConfig = pullToRefreshConfig.stopRefreshing(), + tokensListState = WalletTokensListState.Loading(), + notifications = persistentListOf(), + bottomSheetConfig = null, + tokenActionsBottomSheet = null, + onManageTokensClick = clickIntents::onManageTokensClick, + ) + } + + private fun WalletSingleCurrencyState.Locked.toSingleCurrencyContentState(action: UnlockWalletAction): WalletState { + return WalletSingleCurrencyState.Content( + onBackClick = onBackClick, + topBarConfig = topBarConfig.updateCallback(), + walletsListConfig = walletsListConfig.unlockWallets(action), + pullToRefreshConfig = pullToRefreshConfig.stopRefreshing(), + notifications = persistentListOf(), + bottomSheetConfig = null, + buttons = buttons, + marketPriceBlockState = MarketPriceBlockState.Loading( + currencyName = action.selectedWallet.getPrimaryCurrencyName(), + ), + txHistoryState = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), + ), + ), + ) + } + + private fun WalletTopBarConfig.updateCallback(): WalletTopBarConfig { + return copy(onMoreClick = clickIntents::onDetailsClick) + } + + private fun WalletsListConfig.unlockWallets(action: UnlockWalletAction): WalletsListConfig { + return this.copy( + selectedWalletIndex = action.selectedWalletIndex, + wallets = wallets.unlockWallets(action), + ) + } + + private fun List.unlockWallets(action: UnlockWalletAction): ImmutableList { + return this + .map { prevWallet -> + if (prevWallet is WalletCardState.LockedContent && action.isUnlockedWallet(prevWallet.id)) { + prevWallet.mapToLoadingWalletCardState( + userWallet = action.getUnlockWallet(prevWallet.id), + ) + } else { + prevWallet + } + } + .toImmutableList() + } + + private fun UnlockWalletAction.isUnlockedWallet(walletId: UserWalletId): Boolean { + return unlockedWallets.any { it.walletId == walletId } + } + + private fun UnlockWalletAction.getUnlockWallet(walletId: UserWalletId): UserWallet { + return unlockedWallets.firstOrNull { it.walletId == walletId } + ?: error("Unlocked wallet with id $walletId not found") + } + + private fun WalletCardState.mapToLoadingWalletCardState(userWallet: UserWallet): WalletCardState { + return WalletCardState.Loading( + id = id, + title = title, + additionalInfo = userWallet.createAdditionalInfo(), + imageResId = WalletImageResolver.resolve(cardTypesResolver = userWallet.scanResponse.cardTypesResolver), + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + ) + } + + private fun UserWallet.createAdditionalInfo(): TextReference? { + return if (isMultiCurrency) { + WalletAdditionalInfoFactory.resolve(cardTypesResolver = scanResponse.cardTypesResolver, wallet = this) + } else { + null + } + } + + private fun WalletPullToRefreshConfig.stopRefreshing(): WalletPullToRefreshConfig { + return copy(isRefreshing = false) + } + + private fun UserWallet.getPrimaryCurrencyName(): String { + return scanResponse.cardTypesResolver.getBlockchain().currency + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt index e585ac71f5..9c56350d24 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt @@ -7,6 +7,7 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError +import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents @@ -41,18 +42,34 @@ internal class WalletLoadedTxHistoryConverter( } private fun convertError(error: TxHistoryListError): WalletState { - return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content).copy( - txHistoryState = when (error) { - is TxHistoryListError.DataError -> { - TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) - } - }, - ) + return when (val state = currentStateProvider()) { + is WalletSingleCurrencyState.Content -> { + state.copy( + txHistoryState = when (error) { + is TxHistoryListError.DataError -> { + TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) + } + }, + ) + } + is WalletMultiCurrencyState.Content, + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Locked, + is WalletState.Initial, + -> state + } } private fun convert(items: Flow>): WalletState { - return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content).copy( - txHistoryState = walletTxHistoryItemFlowConverter.convert(value = items), - ) + return when (val state = currentStateProvider()) { + is WalletSingleCurrencyState.Content -> { + state.copy(txHistoryState = walletTxHistoryItemFlowConverter.convert(value = items)) + } + is WalletMultiCurrencyState.Content, + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Locked, + is WalletState.Initial, + -> state + } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt index b8b5ab1a7d..027a2bf2aa 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt @@ -51,11 +51,11 @@ internal class WalletLoadingTxHistoryConverter( } } - private fun convert(value: Int): WalletSingleCurrencyState.Content { - val state = requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content) - val txHistoryContent = requireNotNull(state.txHistoryState as? Content) + private fun convert(value: Int): WalletState { + val state = currentStateProvider() + val txHistoryContent = (state as? WalletSingleCurrencyState.Content)?.txHistoryState as? Content - txHistoryContent.contentItems.update { + txHistoryContent?.contentItems?.update { PagingData.from( data = listOf(TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick)) + MutableList( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index 494e36cee2..fa5c096897 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -163,22 +164,14 @@ private fun CardContainer( var isRenameWalletDialogVisible by rememberSaveable { mutableStateOf(value = false) } - DropdownMenu( - expanded = isMenuVisible, + ManageWalletContextMenu( + isMenuVisible = isMenuVisible, + pressOffset = pressOffset, + itemHeight = itemHeight, onDismissRequest = { isMenuVisible = false }, - modifier = Modifier.background(color = TangemTheme.colors.background.secondary), - offset = pressOffset.copy(y = pressOffset.y - itemHeight), - ) { - MenuItem( - textResId = R.string.common_rename, - imageVector = Icons.Outlined.Edit, - onClick = { - isMenuVisible = false - isRenameWalletDialogVisible = true - }, - ) - MenuItem(textResId = R.string.common_delete, imageVector = Icons.Outlined.Delete, onClick = onDeleteClick) - } + onShowRenameWalletDialogClick = { isRenameWalletDialogVisible = true }, + onDeleteClick = onDeleteClick, + ) if (isRenameWalletDialogVisible) { RenameWalletDialogContent( @@ -192,6 +185,41 @@ private fun CardContainer( } } +@Suppress("LongParameterList") +@Composable +private fun ManageWalletContextMenu( + isMenuVisible: Boolean, + pressOffset: DpOffset, + itemHeight: Dp, + onDismissRequest: () -> Unit, + onShowRenameWalletDialogClick: () -> Unit, + onDeleteClick: () -> Unit, +) { + DropdownMenu( + expanded = isMenuVisible, + onDismissRequest = onDismissRequest, + modifier = Modifier.background(color = TangemTheme.colors.background.secondary), + offset = pressOffset.copy(y = pressOffset.y - itemHeight), + ) { + MenuItem( + textResId = R.string.common_rename, + imageVector = Icons.Outlined.Edit, + onClick = { + onDismissRequest() + onShowRenameWalletDialogClick() + }, + ) + MenuItem( + textResId = R.string.common_delete, + imageVector = Icons.Outlined.Delete, + onClick = { + onDismissRequest() + onDeleteClick() + }, + ) + } +} + @Composable private fun MenuItem(@StringRes textResId: Int, imageVector: ImageVector, onClick: () -> Unit) { DropdownMenuItem( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletSideEffects.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletSideEffects.kt index 0d415eaa74..db3972e010 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletSideEffects.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletSideEffects.kt @@ -19,7 +19,9 @@ import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollec @Composable internal fun WalletSideEffects(lazyListState: LazyListState, walletsListConfig: WalletsListConfig) { LaunchedEffect(key1 = walletsListConfig.selectedWalletIndex) { - lazyListState.scrollToItem(walletsListConfig.selectedWalletIndex) + if (!lazyListState.isScrollInProgress) { + lazyListState.animateScrollToItem(walletsListConfig.selectedWalletIndex) + } } val dragInteraction = lazyListState.interactionSource.interactions.collectAsState(initial = null) 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/ScrollOffsetCollector.kt index 1cfc37798f..ba5548fe3e 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/ScrollOffsetCollector.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.ui.utils +import androidx.compose.foundation.interaction.DragInteraction import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.lazy.LazyListItemInfo import androidx.compose.foundation.lazy.LazyListState @@ -27,7 +28,8 @@ internal class ScrollOffsetCollector( private val LazyListItemInfo.halfItemSize get() = size.div(other = 2) override suspend fun emit(value: List) { - if (!lazyListState.isScrollInProgress || dragInteraction.value == null || value.size <= 1) return + if (isNotUserInteraction() || value.size <= 1) return + val firstItem = value.firstOrNull() ?: return val lastItem = value.lastOrNull() ?: return @@ -37,4 +39,12 @@ internal class ScrollOffsetCollector( callback(lastItem.index - 1) } } + + /** + * Sometimes the list is scrolled programmatically. Example: selecting a specific wallet when a user opens the + * screen for the first time or scans a new wallet. Therefore [ScrollOffsetCollector] should not respond to changes. + */ + private fun isNotUserInteraction(): Boolean { + return !lazyListState.isScrollInProgress || dragInteraction.value !is DragInteraction.Start + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt index 9019ff5b9a..8a05b70ea7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt @@ -6,6 +6,7 @@ import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents @@ -20,7 +21,7 @@ internal class TokenListToWalletStateConverter( private val appCurrencyProvider: Provider, private val isWalletContentHidden: Boolean, clickIntents: WalletClickIntents, -) : Converter { +) : Converter { private val tokenListToContentConverter = TokenListToContentItemsConverter( isWalletContentHidden = isWalletContentHidden, @@ -28,12 +29,20 @@ internal class TokenListToWalletStateConverter( clickIntents = clickIntents, ) - override fun convert(value: TokenList): WalletMultiCurrencyState.Content { - val state = requireNotNull(currentStateProvider() as? WalletMultiCurrencyState.Content) - return state.copy( - walletsListConfig = state.updateSelectedWallet(fiatBalance = value.totalFiatBalance), - tokensListState = tokenListToContentConverter.convert(value = value), - ) + override fun convert(value: TokenList): WalletState { + return when (val state = currentStateProvider()) { + is WalletMultiCurrencyState.Content -> { + state.copy( + walletsListConfig = state.updateSelectedWallet(fiatBalance = value.totalFiatBalance), + tokensListState = tokenListToContentConverter.convert(value = value), + ) + } + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Content, + is WalletSingleCurrencyState.Locked, + is WalletState.Initial, + -> state + } } private fun WalletMultiCurrencyState.updateSelectedWallet(fiatBalance: TokenList.FiatBalance): WalletsListConfig { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt index 34e8f722a8..0dc8e4b35b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt @@ -12,6 +12,10 @@ internal interface WalletClickIntents : TxHistoryClickIntents { fun onScanCardClick() + fun onScanCardNotificationClick() + + fun onScanToUnlockWalletClick() + fun onDetailsClick() fun onBackupCardClick() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt index fb70c489e4..1d981cf79a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt @@ -59,7 +59,7 @@ internal class WalletNotificationsListFactory( } if (tokenList != null && tokenList.hasMissedDerivations()) { - add(element = WalletNotification.ScanCard(onClick = clickIntents::onScanCardClick)) + add(element = WalletNotification.ScanCard(onClick = clickIntents::onScanCardNotificationClick)) } if (isUserAlreadyRateAppCallback()) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateCache.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateCache.kt index c5d4e10424..f508bacc5a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateCache.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateCache.kt @@ -10,13 +10,13 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletState */ internal object WalletStateCache { - private val states = mutableMapOf() + private val states = mutableMapOf() /** Get state by [userWalletId] */ - fun getState(userWalletId: UserWalletId): WalletState? = states[userWalletId] + fun getState(userWalletId: UserWalletId): WalletState.ContentState? = states[userWalletId] /** Add or update [state] by [userWalletId] */ - fun update(userWalletId: UserWalletId, state: WalletState) { + fun update(userWalletId: UserWalletId, state: WalletState.ContentState) { states[userWalletId] = state } } \ 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 5b948416b6..537366f56b 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 @@ -47,6 +47,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -63,7 +64,7 @@ import kotlin.properties.Delegates internal class WalletViewModel @Inject constructor( private val getWalletsUseCase: GetWalletsUseCase, private val saveWalletUseCase: SaveWalletUseCase, - private val getSelectedWalletUseCase: GetSelectedWalletUseCase, + getSelectedWalletUseCase: GetSelectedWalletUseCase, private val selectWalletUseCase: SelectWalletUseCase, private val updateWalletUseCase: UpdateWalletUseCase, private val deleteWalletUseCase: DeleteWalletUseCase, @@ -130,6 +131,11 @@ internal class WalletViewModel @Inject constructor( private val notificationsJobHolder = JobHolder() private val refreshContentJobHolder = JobHolder() + private val walletsUpdateActionResolver = WalletsUpdateActionResolver( + currentStateProvider = Provider { uiState }, + getSelectedWalletUseCase = getSelectedWalletUseCase, + ) + override fun onCreate(owner: LifecycleOwner) { viewModelScope.launch(dispatchers.main) { delay(timeMillis = 1_800) @@ -148,29 +154,48 @@ internal class WalletViewModel @Inject constructor( } private fun updateWallets(sourceList: List) { - if (sourceList.isEmpty()) return - wallets = sourceList - val currentState = uiState - val previousSelectedWalletIndex = (currentState as? WalletState.ContentState) - ?.walletsListConfig - ?.selectedWalletIndex + if (sourceList.isEmpty()) return - val selectedWalletIndex = if (currentState is WalletLockedState) { - currentState.getSelectedWalletIndex() - } else { - val selectedWallet = getSelectedWalletUseCase().fold( - ifLeft = { error("Selected wallet is null") }, - ifRight = { it }, - ) - sourceList.indexOfFirst { it.walletId == selectedWallet.walletId } + when (val action = walletsUpdateActionResolver.resolve(sourceList)) { + is WalletsUpdateActionResolver.Action.InitialWallets -> { + loadAndUpdateState(index = action.selectedWalletIndex) + } + is WalletsUpdateActionResolver.Action.UpdateWalletName -> { + uiState = stateFactory.getStateWithUpdatedWalletName(name = action.name) + } + is WalletsUpdateActionResolver.Action.UnlockWallet -> { + uiState = stateFactory.getUnlockedState(action) + + getContentItemsUpdates(index = action.selectedWalletIndex) + } + is WalletsUpdateActionResolver.Action.DeleteWallet -> { + deleteWalletAndUpdateState(action = action) + } + is WalletsUpdateActionResolver.Action.AddWallet -> { + loadAndUpdateState(index = action.selectedWalletIndex) + } + is WalletsUpdateActionResolver.Action.Unknown -> Unit } + } - if (previousSelectedWalletIndex != selectedWalletIndex) { - uiState = stateFactory.getSkeletonState(wallets = sourceList, selectedWalletIndex = selectedWalletIndex) + private fun loadAndUpdateState(index: Int) { + uiState = stateFactory.getSkeletonState(wallets = wallets, selectedWalletIndex = index) - getContentItemsUpdates(index = selectedWalletIndex) + getContentItemsUpdates(index = index) + } + + private fun deleteWalletAndUpdateState(action: WalletsUpdateActionResolver.Action.DeleteWallet) { + val cacheState = WalletStateCache.getState(userWalletId = action.selectedWalletId) + if (cacheState != null) { + uiState = stateFactory.getStateWithoutDeletedWallet(cacheState, action) + + if (cacheState.isLoadingState()) { + getContentItemsUpdates(action.selectedWalletIndex) + } + } else { + loadAndUpdateState(index = action.selectedWalletIndex) } } @@ -181,23 +206,56 @@ internal class WalletViewModel @Inject constructor( } override fun onScanCardClick() { + viewModelScope.launch(dispatchers.io) { + scanCardProcessor.scan() + .doOnSuccess { + // If card's public key is null then user wallet will be null + val userWallet = UserWalletBuilder(scanResponse = it).build() + + if (userWallet != null) { + saveWalletUseCase(userWallet = userWallet, canOverride = false) + } + } + } + } + + override fun onScanCardNotificationClick() { + scanToUpdateSelectedWallet( + onSuccessSave = { + // Reload currencies with missed derivation + fetchTokenListUseCase(userWalletId = it.walletId) + }, + ) + } + + override fun onScanToUnlockWalletClick() { + scanToUpdateSelectedWallet() + } + + private fun scanToUpdateSelectedWallet(onSuccessSave: suspend (UserWallet) -> Unit = {}) { + val state = uiState as? WalletState.ContentState ?: return + val prevRequestPolicyStatus = getBiometricsStatusUseCase() // Update access the code policy according access code saving status setAccessCodeRequestPolicyUseCase(isBiometricsRequestPolicy = getAccessCodeSavingStatusUseCase()) viewModelScope.launch(dispatchers.io) { - scanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true) + scanCardProcessor.scan( + cardId = getWallet(state.walletsListConfig.selectedWalletIndex).cardId, + allowsRequestAccessCodeFromRepository = true, + ) .doOnSuccess { // If card's public key is null then user wallet will be null val userWallet = UserWalletBuilder(scanResponse = it).build() if (userWallet != null) { - saveWalletUseCase(userWallet) + saveWalletUseCase(userWallet = userWallet, canOverride = true) .onLeft { // Rollback policy if card saving was failed setAccessCodeRequestPolicyUseCase(prevRequestPolicyStatus) } + .onRight { onSuccessSave(userWallet) } } else { // Rollback policy if card saving was failed setAccessCodeRequestPolicyUseCase(prevRequestPolicyStatus) @@ -245,10 +303,7 @@ internal class WalletViewModel @Inject constructor( } override fun onWalletChange(index: Int) { - val state = requireNotNull(uiState as? WalletState.ContentState) { - "Impossible to change wallet if state isn't WalletState.ContentState" - } - + val state = uiState as? WalletState.ContentState ?: return if (state.walletsListConfig.selectedWalletIndex == index) return viewModelScope.launch(dispatchers.io) { @@ -256,15 +311,26 @@ internal class WalletViewModel @Inject constructor( } val cacheState = WalletStateCache.getState(userWalletId = state.walletsListConfig.wallets[index].id) - if (cacheState != null) { - uiState = if (cacheState is WalletState.ContentState) { - cacheState.copySealed( - walletsListConfig = state.walletsListConfig.copy(selectedWalletIndex = index), - pullToRefreshConfig = state.pullToRefreshConfig.copy(isRefreshing = false), - ) - } else { - cacheState - } + if (cacheState != null && cacheState !is WalletLockedState) { + uiState = cacheState.copySealed( + walletsListConfig = state.walletsListConfig.copy( + selectedWalletIndex = index, + wallets = state.walletsListConfig.wallets + .mapIndexed { mapIndex, currentWallet -> + val cacheWallet = cacheState.walletsListConfig.wallets.getOrNull(mapIndex) + + if (currentWallet is WalletCardState.Loading && cacheWallet != null && + cacheWallet.isLoaded() + ) { + cacheWallet + } else { + currentWallet + } + } + .toImmutableList(), + ), + pullToRefreshConfig = cacheState.pullToRefreshConfig.copy(isRefreshing = false), + ) if (cacheState.isLoadingState()) { getContentItemsUpdates(index) @@ -275,6 +341,10 @@ internal class WalletViewModel @Inject constructor( } } + private fun WalletCardState.isLoaded(): Boolean { + return this !is WalletCardState.Loading && this !is WalletCardState.LockedContent + } + override fun onRefreshSwipe() { val selectedWalletIndex = (uiState as? WalletState.ContentState) ?.walletsListConfig @@ -436,10 +506,11 @@ internal class WalletViewModel @Inject constructor( } override fun onDeleteClick(userWalletId: UserWalletId) { + val state = uiState as? WalletState.ContentState ?: return + viewModelScope.launch(dispatchers.io) { val either = deleteWalletUseCase(userWalletId) - val state = requireNotNull(uiState as? WalletState.ContentState) if (state.walletsListConfig.wallets.size <= 1 && either.isRight()) onBackClick() } } @@ -611,9 +682,10 @@ internal class WalletViewModel @Inject constructor( private fun WalletState.isLoadingState(): Boolean { // Check the base components - if (this is WalletState.ContentState) { - walletsListConfig.wallets[walletsListConfig.selectedWalletIndex] is WalletCardState.Loading || - notifications.isEmpty() + if (this is WalletState.ContentState && + walletsListConfig.wallets[walletsListConfig.selectedWalletIndex] is WalletCardState.Loading + ) { + return true } // Check the special components 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 new file mode 100644 index 0000000000..dbcfee3f06 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt @@ -0,0 +1,161 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels + +import com.tangem.common.Provider +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.feature.wallet.presentation.wallet.state.WalletLockedState +import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState + +/** + * Resolver that determines which update action will be performed + * + * @property currentStateProvider current state provider + * @property getSelectedWalletUseCase use case that returns selected wallet + */ +internal class WalletsUpdateActionResolver( + private val currentStateProvider: Provider, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, +) { + + fun resolve(wallets: List): Action { + val selectedWallet = wallets.getSelectedWallet() + + return when (val state = currentStateProvider()) { + is WalletState.Initial -> { + Action.InitialWallets( + selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId), + ) + } + is WalletState.ContentState -> { + getActionToUpdateContent(state = state, wallets = wallets, selectedWallet = selectedWallet) + } + } + } + + private fun List.getSelectedWallet(): UserWallet { + val hasUnlockedWallet = any { !it.isLocked } + return if (hasUnlockedWallet) { + val selectedWalletId = getSelectedWalletUseCase().fold(ifLeft = ::error, ifRight = UserWallet::walletId) + + firstOrNull { it.walletId == selectedWalletId } + ?: error("Wallets don't contain a wallet with id: $selectedWalletId") + } else { + lastOrNull() ?: error("Wallets is empty") + } + } + + private fun getActionToUpdateContent( + state: WalletState.ContentState, + wallets: List, + selectedWallet: UserWallet, + ): Action { + return if (isWalletsCountChanged(state, wallets)) { + getActionToChangeWallets(state = state, wallets = wallets, selectedWallet = selectedWallet) + } else { + getActionToUpdateCurrentWallet(state = state, wallets = wallets, selectedWallet = selectedWallet) + } + } + + private fun isWalletsCountChanged(state: WalletState.ContentState, wallets: List): Boolean { + val prevWalletsSize = state.walletsListConfig.wallets.size + val walletsSize = wallets.size + + return prevWalletsSize != walletsSize + } + + private fun getActionToChangeWallets( + state: WalletState.ContentState, + wallets: List, + selectedWallet: UserWallet, + ): Action { + val prevWalletsSize = state.walletsListConfig.wallets.size + + return when { + prevWalletsSize > wallets.size -> { + Action.DeleteWallet( + selectedWalletId = selectedWallet.walletId, + selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId), + deletedWalletId = state.walletsListConfig.wallets.getDeletedWalletId(wallets), + ) + } + prevWalletsSize < wallets.size -> { + Action.AddWallet( + selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId), + ) + } + else -> Action.Unknown + } + } + + private fun List.getDeletedWalletId(wallets: List): UserWalletId { + return this + .map(WalletCardState::id) + .firstOrNull { !wallets.map(UserWallet::walletId).contains(it) } + ?: error("Deleted wallet id is not found. Wallets contains all previous wallets ids") + } + + private fun getActionToUpdateCurrentWallet( + state: WalletState.ContentState, + wallets: List, + selectedWallet: UserWallet, + ): Action { + val selectedWalletName = selectedWallet.name + + if (state.getPrevSelectedWalletName() != selectedWalletName) { + return Action.UpdateWalletName(selectedWalletName) + } + + if (state is WalletLockedState && !selectedWallet.isLocked) { + return Action.UnlockWallet( + selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId), + selectedWallet = selectedWallet, + unlockedWallets = wallets.filterNot(UserWallet::isLocked), + ) + } + + return Action.Unknown + } + + private fun WalletState.ContentState.getPrevSelectedWalletName(): String { + val prevSelectedWalletIndex = walletsListConfig.selectedWalletIndex + val prevSelectedWallet = walletsListConfig.wallets.getOrNull(prevSelectedWalletIndex) + ?: error("Previous selected wallet is not found") + + return prevSelectedWallet.title + } + + private fun List.indexOfWallet(id: UserWalletId): Int { + val selectedIndex = indexOfFirst { it.walletId == id } + + return if (selectedIndex == -1) { + error("Wallets don't contain a wallet with id: $id") + } else { + selectedIndex + } + } + + sealed class Action { + + data class InitialWallets(val selectedWalletIndex: Int) : Action() + + data class UpdateWalletName(val name: String) : Action() + + data class UnlockWallet( + val selectedWalletIndex: Int, + val selectedWallet: UserWallet, + val unlockedWallets: List, + ) : Action() + + data class DeleteWallet( + val selectedWalletId: UserWalletId, + val selectedWalletIndex: Int, + val deletedWalletId: UserWalletId, + ) : Action() + + data class AddWallet(val selectedWalletIndex: Int) : Action() + + object Unknown : Action() + } +} \ No newline at end of file