Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-05 10:58:42 +03:00
parent 5a169e6211
commit b5e2899332
12 changed files with 375 additions and 380 deletions

View file

@ -7,6 +7,7 @@ import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.feature.wallet.presentation.wallet.state.components.*
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
@ -19,7 +20,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
internal sealed class WalletSingleCurrencyState : WalletState.ContentState() {
/** Manage buttons */
abstract val buttons: ImmutableList<WalletManageButton>
abstract val buttons: PersistentList<WalletManageButton>
/** Transactions history state */
abstract val txHistoryState: TxHistoryState
@ -31,7 +32,7 @@ internal sealed class WalletSingleCurrencyState : WalletState.ContentState() {
override val pullToRefreshConfig: WalletPullToRefreshConfig,
override val notifications: ImmutableList<WalletNotification>,
override val bottomSheetConfig: WalletBottomSheetConfig?,
override val buttons: ImmutableList<WalletManageButton>,
override val buttons: PersistentList<WalletManageButton>,
override val txHistoryState: TxHistoryState,
val marketPriceBlockState: MarketPriceBlockState,
) : WalletSingleCurrencyState()
@ -41,7 +42,7 @@ internal sealed class WalletSingleCurrencyState : WalletState.ContentState() {
override val topBarConfig: WalletTopBarConfig,
override val walletsListConfig: WalletsListConfig,
override val pullToRefreshConfig: WalletPullToRefreshConfig,
override val buttons: ImmutableList<WalletManageButton>,
override val buttons: PersistentList<WalletManageButton>,
override val onUnlockWalletsNotificationClick: () -> Unit,
override val onUnlockClick: () -> Unit,
override val onScanClick: () -> Unit,

View file

@ -8,8 +8,8 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
internal class WalletCryptoCurrencyActionsConverter(
private val currentStateProvider: Provider<WalletState>,
@ -26,7 +26,7 @@ internal class WalletCryptoCurrencyActionsConverter(
}
}
private fun List<TokenActionsState.ActionState>.mapToManageButtons(): ImmutableList<WalletManageButton> {
private fun List<TokenActionsState.ActionState>.mapToManageButtons(): PersistentList<WalletManageButton> {
return this
.mapNotNull { action ->
when (action) {
@ -45,6 +45,6 @@ internal class WalletCryptoCurrencyActionsConverter(
is TokenActionsState.ActionState.Swap -> null
}
}
.toImmutableList()
.toPersistentList()
}
}

View file

@ -9,7 +9,6 @@ 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.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletLoadedTokensListConverter.LoadedTokensListModel
import com.tangem.feature.wallet.presentation.wallet.utils.TokenListErrorConverter
import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
@ -27,11 +26,12 @@ import com.tangem.utils.converter.Converter
*/
internal class WalletLoadedTokensListConverter(
private val currentStateProvider: Provider<WalletState>,
private val tokenListErrorConverter: TokenListErrorConverter,
appCurrencyProvider: Provider<AppCurrency>,
cardTypeResolverProvider: Provider<CardTypesResolver>,
currentWalletProvider: Provider<UserWallet>,
clickIntents: WalletClickIntents,
) : Converter<LoadedTokensListModel, WalletState> {
) : Converter<Either<TokenListError, TokenList>, WalletState> {
private val tokenListStateConverter = TokenListToWalletStateConverter(
currentStateProvider = currentStateProvider,
@ -42,26 +42,10 @@ internal class WalletLoadedTokensListConverter(
clickIntents = clickIntents,
)
private val tokenListErrorStateConverter = TokenListErrorConverter(
currentStateProvider = currentStateProvider,
)
override fun convert(value: LoadedTokensListModel): WalletState {
return value.tokenListEither.fold(
ifLeft = tokenListErrorStateConverter::convert,
ifRight = {
tokenListStateConverter.convert(
value = TokenListToWalletStateConverter.TokensListModel(
tokenList = it,
isRefreshing = value.isRefreshing,
),
)
},
override fun convert(value: Either<TokenListError, TokenList>): WalletState {
return value.fold(
ifLeft = tokenListErrorConverter::convert,
ifRight = tokenListStateConverter::convert,
)
}
data class LoadedTokensListModel(
val tokenListEither: Either<TokenListError, TokenList>,
val isRefreshing: Boolean,
)
}

View file

@ -13,7 +13,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopB
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.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@ -99,7 +99,7 @@ internal class WalletLockedConverter(
)
}
private fun createButtons(): ImmutableList<WalletManageButton> {
private fun createButtons(): PersistentList<WalletManageButton> {
return persistentListOf(
WalletManageButton.Buy(enabled = false, onClick = {}),
WalletManageButton.Send(enabled = false, onClick = {}),

View file

@ -1,124 +1,112 @@
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.domain.common.CardTypesResolver
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
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.state.components.WalletTokensListState.TokensListItemState
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.WalletTokensListState
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.update
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.mutate
internal class WalletRefreshStateConverter(
private val currentStateProvider: Provider<WalletState>,
private val currentCardTypeResolverProvider: Provider<CardTypesResolver>,
private val clickIntents: WalletClickIntents,
) : Converter<Unit, WalletState> {
private val intents: WalletClickIntents,
) : Converter<Boolean, WalletState> {
override fun convert(value: Unit): WalletState {
return when (val state = currentStateProvider()) {
is WalletMultiCurrencyState.Content -> state.getRefreshState()
is WalletSingleCurrencyState.Content -> state.getRefreshState()
else -> state
}
}
override fun convert(value: Boolean): WalletState {
val state = currentStateProvider()
val contentState = state as? WalletState.ContentState ?: return state
private fun WalletMultiCurrencyState.Content.getRefreshState(): WalletMultiCurrencyState.Content {
return copy(
walletsListConfig = createWalletsListConfig(),
pullToRefreshConfig = createPullToRefreshConfig(),
tokensListState = createTokenListState(),
)
}
private fun WalletSingleCurrencyState.Content.getRefreshState(): WalletSingleCurrencyState.Content {
return copy(
walletsListConfig = createWalletsListConfig(),
pullToRefreshConfig = createPullToRefreshConfig(),
buttons = buttons.mapToDisabledButton(),
txHistoryState = createTxHistoryState(),
marketPriceBlockState = MarketPriceBlockState.Loading(currencyName = marketPriceBlockState.currencyName),
)
}
private fun WalletState.ContentState.createWalletsListConfig(): WalletsListConfig {
val selectedWallet = walletsListConfig.wallets[walletsListConfig.selectedWalletIndex]
val additionalInfo = if (currentCardTypeResolverProvider().isMultiwalletAllowed()) {
selectedWallet.additionalInfo
return if (value) {
contentState.getRefreshingState()
} else {
null
contentState.getRefreshedState()
}
}
return walletsListConfig.copy(
wallets = walletsListConfig.wallets.toPersistentList().set(
index = walletsListConfig.selectedWalletIndex,
element = WalletCardState.Loading(
id = selectedWallet.id,
title = selectedWallet.title,
additionalInfo = additionalInfo,
imageResId = selectedWallet.imageResId,
onRenameClick = selectedWallet.onRenameClick,
onDeleteClick = selectedWallet.onDeleteClick,
),
),
private fun WalletState.ContentState.getRefreshingState(): WalletState {
return when (this) {
is WalletMultiCurrencyState.Content -> getRefreshingState()
is WalletSingleCurrencyState.Content -> getRefreshingState()
is WalletMultiCurrencyState.Locked,
is WalletSingleCurrencyState.Locked,
-> this
}
}
private fun WalletState.ContentState.getRefreshedState(): WalletState {
return when (this) {
is WalletMultiCurrencyState.Content -> getRefreshedState()
is WalletSingleCurrencyState.Content -> getRefreshedState()
is WalletMultiCurrencyState.Locked,
is WalletSingleCurrencyState.Locked,
-> this
}
}
private fun WalletMultiCurrencyState.Content.getRefreshingState(): WalletMultiCurrencyState {
return copy(
pullToRefreshConfig = updatePullToRefreshConfig(isRefreshing = true),
tokensListState = updateTokenListState(isRefreshing = true),
)
}
private fun WalletState.ContentState.createPullToRefreshConfig(): WalletPullToRefreshConfig {
return pullToRefreshConfig.copy(isRefreshing = true)
private fun WalletSingleCurrencyState.Content.getRefreshingState(): WalletSingleCurrencyState {
return copy(
pullToRefreshConfig = updatePullToRefreshConfig(isRefreshing = true),
buttons = updateButtons(isRefreshing = true),
)
}
private fun WalletMultiCurrencyState.Content.createTokenListState(): WalletTokensListState {
return when (tokensListState) {
private fun WalletMultiCurrencyState.Content.getRefreshedState(): WalletMultiCurrencyState {
return copy(
pullToRefreshConfig = updatePullToRefreshConfig(isRefreshing = false),
tokensListState = updateTokenListState(isRefreshing = false),
)
}
private fun WalletSingleCurrencyState.Content.getRefreshedState(): WalletSingleCurrencyState {
return copy(
pullToRefreshConfig = updatePullToRefreshConfig(isRefreshing = false),
buttons = updateButtons(isRefreshing = false),
)
}
private fun WalletMultiCurrencyState.updateTokenListState(isRefreshing: Boolean): WalletTokensListState {
return when (val state = tokensListState) {
is WalletTokensListState.Content -> {
WalletTokensListState.Loading(
items = tokensListState.items
.filterIsInstance<TokensListItemState.Token>()
.mapToLoadingTokenState(),
)
val onOrganizeTokensClick = if (isRefreshing) null else intents::onOrganizeTokensClick
state.copy(onOrganizeTokensClick = onOrganizeTokensClick)
}
is WalletTokensListState.Empty -> WalletTokensListState.Loading()
is WalletTokensListState.Loading,
is WalletTokensListState.Locked,
-> tokensListState
is WalletTokensListState.Loading,
is WalletTokensListState.Empty,
-> state
}
}
private fun List<TokensListItemState.Token>.mapToLoadingTokenState(): ImmutableList<TokensListItemState.Token> {
return this
.map { TokensListItemState.Token(state = TokenItemState.Loading(id = it.state.id)) }
.toImmutableList()
}
private fun WalletSingleCurrencyState.updateButtons(isRefreshing: Boolean): PersistentList<WalletManageButton> {
val isButtonsEnabled = !isRefreshing
private fun ImmutableList<WalletManageButton>.mapToDisabledButton(): ImmutableList<WalletManageButton> {
return this
.mapNotNull { button ->
return buttons.mutate {
it.mapNotNull { button ->
when (button) {
is WalletManageButton.Buy -> button.copy(enabled = false)
is WalletManageButton.Send -> button.copy(enabled = false)
is WalletManageButton.Buy -> button.copy(enabled = isButtonsEnabled)
is WalletManageButton.Send -> button.copy(enabled = isButtonsEnabled)
is WalletManageButton.Sell -> button.copy(enabled = isButtonsEnabled)
is WalletManageButton.Receive -> button
is WalletManageButton.Sell -> button.copy(enabled = false)
is WalletManageButton.Swap -> null
}
}
.toImmutableList()
}
}
private fun WalletSingleCurrencyState.Content.createTxHistoryState(): TxHistoryState {
if (txHistoryState is TxHistoryState.Content) {
txHistoryState.contentItems.update {
TxHistoryState.getDefaultLoadingTransactions(onExploreClick = clickIntents::onExploreClick)
}
}
return txHistoryState
private fun WalletState.ContentState.updatePullToRefreshConfig(isRefreshing: Boolean): WalletPullToRefreshConfig {
return pullToRefreshConfig.copy(isRefreshing = isRefreshing)
}
}

View file

@ -15,7 +15,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyS
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.WalletsListConfig
import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletSingleCurrencyLoadedBalanceConverter.SingleCurrencyLoadedBalanceModel
import com.tangem.feature.wallet.presentation.wallet.utils.CurrencyStatusErrorConverter
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
@ -25,32 +25,21 @@ internal class WalletSingleCurrencyLoadedBalanceConverter(
private val cardTypeResolverProvider: Provider<CardTypesResolver>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val currentWalletProvider: Provider<UserWallet>,
) : Converter<SingleCurrencyLoadedBalanceModel, WalletSingleCurrencyState.Content> {
private val currencyStatusErrorConverter: CurrencyStatusErrorConverter,
) : Converter<Either<CurrencyStatusError, CryptoCurrencyStatus>, WalletSingleCurrencyState.Content> {
override fun convert(value: SingleCurrencyLoadedBalanceModel): WalletSingleCurrencyState.Content {
return value.cryptoCurrencyEither.fold(
ifLeft = { convertError() },
ifRight = { convertContent(it, value.isRefreshing) },
override fun convert(value: Either<CurrencyStatusError, CryptoCurrencyStatus>): WalletSingleCurrencyState.Content {
return value.fold(
ifLeft = currencyStatusErrorConverter::convert,
ifRight = ::convertContent,
)
}
private fun convertError(): WalletSingleCurrencyState.Content {
return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content)
}
private fun convertContent(
status: CryptoCurrencyStatus,
isRefreshing: Boolean,
): WalletSingleCurrencyState.Content {
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),
pullToRefreshConfig = if (isRefreshing) {
state.pullToRefreshConfig.copy(isRefreshing = status.value is CryptoCurrencyStatus.Loading)
} else {
state.pullToRefreshConfig
},
marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName),
)
}
@ -168,9 +157,4 @@ internal class WalletSingleCurrencyLoadedBalanceConverter(
fiatCurrencySymbol = appCurrency.symbol,
)
}
data class SingleCurrencyLoadedBalanceModel(
val cryptoCurrencyEither: Either<CurrencyStatusError, CryptoCurrencyStatus>,
val isRefreshing: Boolean,
)
}

View file

@ -14,7 +14,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.*
import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletSkeletonStateConverter.SkeletonModel
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
@ -128,7 +128,7 @@ internal class WalletSkeletonStateConverter(
return WalletPullToRefreshConfig(isRefreshing = false, onRefresh = clickIntents::onRefreshSwipe)
}
private fun createButtons(): ImmutableList<WalletManageButton> {
private fun createButtons(): PersistentList<WalletManageButton> {
return persistentListOf(
WalletManageButton.Buy(enabled = false, onClick = {}),
WalletManageButton.Send(enabled = false, onClick = {}),

View file

@ -22,6 +22,8 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.WalletBott
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification
import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.WalletLoadedTxHistoryConverter
import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.WalletLoadingTxHistoryConverter
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 kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.Flow
@ -45,9 +47,16 @@ internal class WalletStateFactory(
private val tokenActionsProvider by lazy { TokenActionsProvider(currentStateProvider = currentStateProvider) }
private val skeletonConverter by lazy { WalletSkeletonStateConverter(currentStateProvider, clickIntents) }
private val tokenListErrorConverter by lazy {
TokenListErrorConverter(currentStateProvider)
}
private val currencyStatusErrorConverter by lazy {
CurrencyStatusErrorConverter(currentStateProvider)
}
private val loadedTokensListConverter by lazy {
WalletLoadedTokensListConverter(
currentStateProvider = currentStateProvider,
tokenListErrorConverter = tokenListErrorConverter,
cardTypeResolverProvider = currentCardTypeResolverProvider,
currentWalletProvider = currentWalletProvider,
appCurrencyProvider = appCurrencyProvider,
@ -76,6 +85,7 @@ internal class WalletStateFactory(
cardTypeResolverProvider = currentCardTypeResolverProvider,
appCurrencyProvider = appCurrencyProvider,
currentWalletProvider = currentWalletProvider,
currencyStatusErrorConverter = currencyStatusErrorConverter,
)
}
@ -91,8 +101,7 @@ internal class WalletStateFactory(
private val refreshStateConverter by lazy {
WalletRefreshStateConverter(
currentStateProvider = currentStateProvider,
currentCardTypeResolverProvider = currentCardTypeResolverProvider,
clickIntents = clickIntents,
intents = clickIntents,
)
}
@ -114,13 +123,12 @@ internal class WalletStateFactory(
)
}
fun getStateByTokensList(tokenListEither: Either<TokenListError, TokenList>, isRefreshing: Boolean): WalletState {
return loadedTokensListConverter.convert(
value = WalletLoadedTokensListConverter.LoadedTokensListModel(
tokenListEither = tokenListEither,
isRefreshing = isRefreshing,
),
)
fun getStateByTokensList(maybeTokenList: Either<TokenListError, TokenList>): WalletState {
return loadedTokensListConverter.convert(maybeTokenList)
}
fun getStateByTokenListError(error: TokenListError): WalletState {
return tokenListErrorConverter.convert(error)
}
fun getStateByNotifications(notifications: ImmutableList<WalletNotification>): WalletState {
@ -131,7 +139,9 @@ internal class WalletStateFactory(
}
}
fun getStateAfterContentRefreshing(): WalletState = refreshStateConverter.convert(Unit)
fun getRefreshingState(): WalletState = refreshStateConverter.convert(value = true)
fun getRefreshedState(): WalletState = refreshStateConverter.convert(value = false)
fun getStateWithOpenWalletBottomSheet(content: WalletBottomSheetConfig.BottomSheetContentConfig): WalletState {
return when (val state = currentStateProvider() as WalletState.ContentState) {
@ -157,6 +167,7 @@ internal class WalletStateFactory(
isBottomSheetShow = true,
onBottomSheetDismiss = clickIntents::onDismissBottomSheet,
)
else -> state
}
}
@ -170,6 +181,7 @@ internal class WalletStateFactory(
bottomSheetConfig = state.bottomSheetConfig?.copy(isShow = false),
)
is WalletSingleCurrencyState.Locked -> state.copy(isBottomSheetShow = false)
else -> state
}
}
@ -199,18 +211,16 @@ internal class WalletStateFactory(
fun getLockedState(): WalletState = lockedConverter.convert(Unit)
fun getSingleCurrencyLoadedBalanceState(
cryptoCurrencyEither: Either<CurrencyStatusError, CryptoCurrencyStatus>,
isRefreshing: Boolean,
maybeCryptoCurrencyStatus: Either<CurrencyStatusError, CryptoCurrencyStatus>,
): WalletState {
return singleCurrencyLoadedBalanceConverter.convert(
value = WalletSingleCurrencyLoadedBalanceConverter.SingleCurrencyLoadedBalanceModel(
cryptoCurrencyEither = cryptoCurrencyEither,
isRefreshing = isRefreshing,
),
)
return singleCurrencyLoadedBalanceConverter.convert(maybeCryptoCurrencyStatus)
}
fun getSingleCurrencyManageButtonsState(actions: List<TokenActionsState.ActionState>): WalletState {
return cryptoCurrencyActionsConverter.convert(value = actions)
}
fun getStateByCurrencyStatusError(error: CurrencyStatusError): WalletState {
return currencyStatusErrorConverter.convert(error)
}
}

View file

@ -1,15 +1,15 @@
package com.tangem.feature.wallet.presentation.wallet.ui
import androidx.activity.compose.BackHandler
import androidx.compose.animation.*
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource

View file

@ -0,0 +1,17 @@
package com.tangem.feature.wallet.presentation.wallet.utils
import com.tangem.common.Converter
import com.tangem.common.Provider
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
// TODO: Implement this
internal class CurrencyStatusErrorConverter(
private val currentStateProvider: Provider<WalletState>,
) : Converter<CurrencyStatusError, WalletSingleCurrencyState.Content> {
override fun convert(value: CurrencyStatusError): WalletSingleCurrencyState.Content {
return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content)
}
}

View file

@ -8,7 +8,6 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
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.utils.TokenListToWalletStateConverter.TokensListModel
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toPersistentList
@ -21,7 +20,7 @@ internal class TokenListToWalletStateConverter(
private val appCurrencyProvider: Provider<AppCurrency>,
private val isWalletContentHidden: Boolean,
clickIntents: WalletClickIntents,
) : Converter<TokensListModel, WalletMultiCurrencyState.Content> {
) : Converter<TokenList, WalletMultiCurrencyState.Content> {
private val tokenListToContentConverter = TokenListToContentItemsConverter(
isWalletContentHidden = isWalletContentHidden,
@ -29,16 +28,11 @@ internal class TokenListToWalletStateConverter(
clickIntents = clickIntents,
)
override fun convert(value: TokensListModel): WalletMultiCurrencyState.Content {
override fun convert(value: TokenList): WalletMultiCurrencyState.Content {
val state = requireNotNull(currentStateProvider() as? WalletMultiCurrencyState.Content)
return state.copy(
walletsListConfig = state.updateSelectedWallet(fiatBalance = value.tokenList.totalFiatBalance),
pullToRefreshConfig = if (value.isRefreshing) {
state.pullToRefreshConfig.copy(isRefreshing = getRefreshingStatus(tokenList = value.tokenList))
} else {
state.pullToRefreshConfig
},
tokensListState = tokenListToContentConverter.convert(value = value.tokenList),
walletsListConfig = state.updateSelectedWallet(fiatBalance = value.totalFiatBalance),
tokensListState = tokenListToContentConverter.convert(value = value),
)
}
@ -58,10 +52,4 @@ internal class TokenListToWalletStateConverter(
.set(index = selectedWalletIndex, element = converter.convert(fiatBalance)),
)
}
private fun getRefreshingStatus(tokenList: TokenList): Boolean {
return tokenList.totalFiatBalance is TokenList.FiatBalance.Loading
}
data class TokensListModel(val tokenList: TokenList, val isRefreshing: Boolean)
}

View file

@ -21,9 +21,7 @@ import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase
import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TokenList
@ -73,7 +71,9 @@ internal class WalletViewModel @Inject constructor(
private val setAccessCodeRequestPolicyUseCase: SetAccessCodeRequestPolicyUseCase,
private val getAccessCodeSavingStatusUseCase: GetAccessCodeSavingStatusUseCase,
private val getTokenListUseCase: GetTokenListUseCase,
private val getPrimaryCurrencyUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
private val fetchTokenListUseCase: FetchTokenListUseCase,
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
private val getCardWasScannedUseCase: GetCardWasScannedUseCase,
private val isUserAlreadyRateAppUseCase: IsUserAlreadyRateAppUseCase,
private val isDemoCardUseCase: IsDemoCardUseCase,
@ -127,6 +127,7 @@ internal class WalletViewModel @Inject constructor(
private val marketPriceJobHolder = JobHolder()
private val buttonsJobHolder = JobHolder()
private val notificationsJobHolder = JobHolder()
private val refreshContentJobHolder = JobHolder()
override fun onCreate(owner: LifecycleOwner) {
viewModelScope.launch(dispatchers.main) {
@ -151,6 +152,10 @@ internal class WalletViewModel @Inject constructor(
wallets = sourceList
val currentState = uiState
val previousSelectedWalletIndex = (currentState as? WalletState.ContentState)
?.walletsListConfig
?.selectedWalletIndex
val selectedWalletIndex = if (currentState is WalletLockedState) {
currentState.getSelectedWalletIndex()
} else {
@ -161,141 +166,13 @@ internal class WalletViewModel @Inject constructor(
sourceList.indexOfFirst { it.walletId == selectedWallet.walletId }
}
uiState = stateFactory.getSkeletonState(wallets = sourceList, selectedWalletIndex = selectedWalletIndex)
if (previousSelectedWalletIndex != selectedWalletIndex) {
uiState = stateFactory.getSkeletonState(wallets = sourceList, selectedWalletIndex = selectedWalletIndex)
updateContentItems(index = selectedWalletIndex)
}
private fun updateContentItems(index: Int, isRefreshing: Boolean = false) {
val cardTypeResolver = getCardTypeResolver(index)
when {
getWallet(index).isLocked -> uiState = stateFactory.getLockedState()
cardTypeResolver.isMultiwalletAllowed() -> updateMultiCurrencyContent(index, isRefreshing)
!cardTypeResolver.isMultiwalletAllowed() -> updateSingleCurrencyContent(index, isRefreshing)
getContentItemsUpdates(index = selectedWalletIndex)
}
}
private fun updateMultiCurrencyContent(index: Int, isRefreshing: Boolean = false) {
val state = requireNotNull(uiState as? WalletMultiCurrencyState) {
"Impossible to update tokens list if state isn't WalletMultiCurrencyState"
}
getTokenListUseCase(userWalletId = state.walletsListConfig.wallets[index].id)
.distinctUntilChanged()
.onEach { tokenListEither ->
uiState = stateFactory.getStateByTokensList(
tokenListEither = tokenListEither,
isRefreshing = isRefreshing,
)
updateNotifications(
index = index,
tokenList = tokenListEither.fold(ifLeft = { null }, ifRight = { it }),
)
}
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
.saveIn(tokensJobHolder)
}
private fun updateSingleCurrencyContent(index: Int, isRefreshing: Boolean) {
val wallet = getWallet(index)
val blockchain = getCardTypeResolver(index).getBlockchain()
updateTxHistory(
blockchain = blockchain,
derivationStyle = wallet.scanResponse.derivationStyleProvider.getDerivationStyle(),
)
updateMarketPrice(userWalletId = wallet.walletId, isRefreshing = isRefreshing)
updateNotifications(index)
}
private fun updateTxHistory(blockchain: Blockchain, derivationStyle: DerivationStyle?) {
viewModelScope.launch(dispatchers.io) {
val derivationPath = blockchain.derivationPath(style = derivationStyle)?.rawPath
val txHistoryItemsCountEither = txHistoryItemsCountUseCase(
networkId = Network.ID(blockchain.id),
derivationPath = derivationPath,
)
uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither)
txHistoryItemsCountEither.onRight {
uiState = stateFactory.getLoadedTxHistoryState(
txHistoryEither = txHistoryItemsUseCase(
networkId = Network.ID(blockchain.id),
derivationPath = derivationPath,
).map {
it.cachedIn(viewModelScope)
},
)
}
}
}
// It also update wallet balance
private fun updateMarketPrice(userWalletId: UserWalletId, isRefreshing: Boolean) {
getPrimaryCurrencyUseCase(userWalletId = userWalletId)
.distinctUntilChanged()
.onEach { either ->
uiState = stateFactory.getSingleCurrencyLoadedBalanceState(
cryptoCurrencyEither = either,
isRefreshing = isRefreshing,
)
either.onRight { status ->
cryptoCurrencyStatus = status
updateButtons(userWalletId = userWalletId, currency = status.currency)
}
}
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
.saveIn(marketPriceJobHolder)
}
private fun updateButtons(userWalletId: UserWalletId, currency: CryptoCurrency) {
getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, cryptoCurrency = currency)
.distinctUntilChanged()
.onEach { uiState = stateFactory.getSingleCurrencyManageButtonsState(actions = it.states) }
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
.saveIn(buttonsJobHolder)
}
private fun updateNotifications(index: Int, tokenList: TokenList? = null) {
notificationsListFactory.create(
cardTypesResolver = getCardTypeResolver(index = index),
tokenList = tokenList,
)
.distinctUntilChanged()
.onEach { uiState = stateFactory.getStateByNotifications(notifications = it) }
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
.saveIn(notificationsJobHolder)
}
override fun onStop(owner: LifecycleOwner) {
viewModelScope.launch(dispatchers.io) {
saveSelectedWallet()
}
}
private suspend fun saveSelectedWallet() {
val state = uiState
if (state is WalletState.ContentState) {
selectWalletUseCase(getWallet(index = state.walletsListConfig.selectedWalletIndex).walletId)
}
}
private fun getWallet(index: Int): UserWallet {
return requireNotNull(
value = wallets.getOrNull(index),
lazyMessage = { "WalletsList doesn't contain element with index = $index" },
)
}
private fun getCardTypeResolver(index: Int): CardTypesResolver = getWallet(index).scanResponse.cardTypesResolver
override fun onBackClick() {
viewModelScope.launch(dispatchers.main) {
router.popBackStack(screen = if (shouldSaveUserWalletsUseCase()) AppScreen.Welcome else AppScreen.Home)
@ -373,14 +250,9 @@ internal class WalletViewModel @Inject constructor(
if (state.walletsListConfig.selectedWalletIndex == index) return
/*
* When wallet is changed it's necessary to stop the last jobs.
* If jobs aren't stopped and wallet is changed then it will update state for the prev wallet.
*/
tokensJobHolder.update(job = null)
marketPriceJobHolder.update(job = null)
buttonsJobHolder.update(job = null)
notificationsJobHolder.update(job = null)
viewModelScope.launch(dispatchers.io) {
selectWalletUseCase(getWallet(index = index).walletId)
}
val cacheState = WalletStateCache.getState(userWalletId = state.walletsListConfig.wallets[index].id)
if (cacheState != null) {
@ -393,50 +265,28 @@ internal class WalletViewModel @Inject constructor(
cacheState
}
if (cacheState.isLoadingState()) updateContentItems(index)
if (cacheState.isLoadingState()) {
getContentItemsUpdates(index)
}
} else {
uiState = stateFactory.getSkeletonState(wallets = wallets, selectedWalletIndex = index)
updateContentItems(index = index)
}
}
private fun WalletState.isLoadingState(): Boolean {
// Check the base components
if (this is WalletState.ContentState) {
walletsListConfig.wallets[walletsListConfig.selectedWalletIndex] is WalletCardState.Loading ||
notifications.isEmpty()
}
// Check the special components
return when (this) {
is WalletMultiCurrencyState -> {
val hasLoadingTokens = tokensListState is WalletTokensListState.ContentState &&
(tokensListState as WalletTokensListState.ContentState).items
.filterIsInstance<WalletTokensListState.TokensListItemState.Token>()
.any { it.state is TokenItemState.Loading }
tokensListState is WalletTokensListState.Loading || hasLoadingTokens
}
is WalletSingleCurrencyState -> {
this is WalletSingleCurrencyState.Content && marketPriceBlockState is MarketPriceBlockState.Loading
}
is WalletState.Initial -> false
getContentItemsUpdates(index = index)
}
}
override fun onRefreshSwipe() {
if (uiState is WalletState.Initial || uiState is WalletLockedState) return
val selectedWalletIndex = (uiState as? WalletState.ContentState)
?.walletsListConfig
?.selectedWalletIndex
?: return
viewModelScope.launch(dispatchers.io) {
uiState = stateFactory.getStateAfterContentRefreshing()
// TODO: [REDACTED_JIRA]
delay(timeMillis = 500)
updateContentItems(
index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex,
isRefreshing = true,
)
when (uiState) {
is WalletMultiCurrencyState.Content -> refreshMultiCurrencyContent(selectedWalletIndex)
is WalletSingleCurrencyState.Content -> refreshSingleCurrencyContent(selectedWalletIndex)
is WalletState.Initial,
is WalletMultiCurrencyState.Locked,
is WalletSingleCurrencyState.Locked,
-> Unit
}
}
@ -486,11 +336,12 @@ internal class WalletViewModel @Inject constructor(
}
override fun onReloadClick() {
uiState = stateFactory.getStateAfterContentRefreshing()
updateSingleCurrencyContent(
index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex,
isRefreshing = true,
)
val selectedWalletIndex = (uiState as? WalletSingleCurrencyState)
?.walletsListConfig
?.selectedWalletIndex
?: return
refreshSingleCurrencyContent(selectedWalletIndex)
}
override fun onExploreClick() {
@ -553,18 +404,6 @@ internal class WalletViewModel @Inject constructor(
}
}
private fun createSelectedAppCurrencyFlow(): StateFlow<AppCurrency> {
return getSelectedAppCurrencyUseCase()
.map { maybeAppCurrency ->
maybeAppCurrency.getOrElse { AppCurrency.Default }
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = AppCurrency.Default,
)
}
override fun onDismissBottomSheet() {
uiState = stateFactory.getStateWithClosedBottomSheet()
}
@ -578,4 +417,188 @@ internal class WalletViewModel @Inject constructor(
)
}
}
private fun getContentItemsUpdates(index: Int) {
/*
* When wallet is changed it's necessary to stop the last jobs.
* If jobs aren't stopped and wallet is changed then it will update state for the prev wallet.
*/
tokensJobHolder.update(job = null)
marketPriceJobHolder.update(job = null)
buttonsJobHolder.update(job = null)
notificationsJobHolder.update(job = null)
refreshContentJobHolder.update(job = null)
val wallet = getWallet(index)
when {
wallet.isLocked -> {
uiState = stateFactory.getLockedState()
}
wallet.isMultiCurrency -> getMultiCurrencyContent(index)
!wallet.isMultiCurrency -> getSingleCurrencyContent(index)
}
}
private fun getMultiCurrencyContent(walletIndex: Int) {
val state = requireNotNull(uiState as? WalletMultiCurrencyState) {
"Impossible to get a token list updates if state isn't WalletMultiCurrencyState"
}
getTokenListUseCase(userWalletId = state.walletsListConfig.wallets[walletIndex].id)
.distinctUntilChanged()
.onEach { maybeTokenList ->
uiState = stateFactory.getStateByTokensList(maybeTokenList)
updateNotifications(
index = walletIndex,
tokenList = maybeTokenList.fold(ifLeft = { null }, ifRight = { it }),
)
}
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
.saveIn(tokensJobHolder)
}
private fun getSingleCurrencyContent(index: Int) {
val wallet = getWallet(index)
val blockchain = getCardTypeResolver(index).getBlockchain()
updateTxHistory(
blockchain = blockchain,
derivationStyle = wallet.scanResponse.derivationStyleProvider.getDerivationStyle(),
)
updateMarketPrice(userWalletId = wallet.walletId)
updateNotifications(index)
}
private fun updateTxHistory(blockchain: Blockchain, derivationStyle: DerivationStyle?) {
viewModelScope.launch(dispatchers.io) {
val derivationPath = blockchain.derivationPath(style = derivationStyle)?.rawPath
val txHistoryItemsCountEither = txHistoryItemsCountUseCase(
networkId = Network.ID(blockchain.id),
derivationPath = derivationPath,
)
uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither)
txHistoryItemsCountEither.onRight {
uiState = stateFactory.getLoadedTxHistoryState(
txHistoryEither = txHistoryItemsUseCase(
networkId = Network.ID(blockchain.id),
derivationPath = derivationPath,
).map {
it.cachedIn(viewModelScope)
},
)
}
}
}
// It also update wallet balance
private fun updateMarketPrice(userWalletId: UserWalletId) {
getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId)
.distinctUntilChanged()
.onEach { maybeCryptoCurrencyStatus ->
uiState = stateFactory.getSingleCurrencyLoadedBalanceState(maybeCryptoCurrencyStatus)
maybeCryptoCurrencyStatus.onRight { status ->
cryptoCurrencyStatus = status
updateButtons(userWalletId = userWalletId, currency = status.currency)
}
}
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
.saveIn(marketPriceJobHolder)
}
private fun updateButtons(userWalletId: UserWalletId, currency: CryptoCurrency) {
getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, cryptoCurrency = currency)
.distinctUntilChanged()
.onEach { uiState = stateFactory.getSingleCurrencyManageButtonsState(actions = it.states) }
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
.saveIn(buttonsJobHolder)
}
private fun updateNotifications(index: Int, tokenList: TokenList? = null) {
notificationsListFactory.create(
cardTypesResolver = getCardTypeResolver(index = index),
tokenList = tokenList,
)
.distinctUntilChanged()
.onEach { uiState = stateFactory.getStateByNotifications(notifications = it) }
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
.saveIn(notificationsJobHolder)
}
private fun refreshMultiCurrencyContent(walletIndex: Int) {
uiState = stateFactory.getRefreshingState()
val wallet = getWallet(walletIndex)
viewModelScope.launch(dispatchers.io) {
val result = fetchTokenListUseCase(wallet.walletId, refresh = true)
uiState = stateFactory.getRefreshedState()
uiState = result.fold(stateFactory::getStateByTokenListError) { uiState }
}.saveIn(refreshContentJobHolder)
}
private fun refreshSingleCurrencyContent(walletIndex: Int) {
uiState = stateFactory.getRefreshingState()
val wallet = getWallet(walletIndex)
viewModelScope.launch(dispatchers.io) {
val result = fetchCurrencyStatusUseCase(wallet.walletId, refresh = true)
uiState = stateFactory.getRefreshedState()
uiState = result.fold(stateFactory::getStateByCurrencyStatusError) { uiState }
}.saveIn(refreshContentJobHolder)
}
private fun createSelectedAppCurrencyFlow(): StateFlow<AppCurrency> {
return getSelectedAppCurrencyUseCase()
.map { maybeAppCurrency ->
maybeAppCurrency.getOrElse { AppCurrency.Default }
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = AppCurrency.Default,
)
}
private fun WalletState.isLoadingState(): Boolean {
// Check the base components
if (this is WalletState.ContentState) {
walletsListConfig.wallets[walletsListConfig.selectedWalletIndex] is WalletCardState.Loading ||
notifications.isEmpty()
}
// Check the special components
return when (this) {
is WalletMultiCurrencyState -> {
val hasLoadingTokens = tokensListState is WalletTokensListState.ContentState &&
(tokensListState as WalletTokensListState.ContentState).items
.filterIsInstance<WalletTokensListState.TokensListItemState.Token>()
.any { it.state is TokenItemState.Loading }
tokensListState is WalletTokensListState.Loading || hasLoadingTokens
}
is WalletSingleCurrencyState -> {
this is WalletSingleCurrencyState.Content && marketPriceBlockState is MarketPriceBlockState.Loading
}
is WalletState.Initial -> false
}
}
private fun getWallet(index: Int): UserWallet {
return requireNotNull(
value = wallets.getOrNull(index),
lazyMessage = { "WalletsList doesn't contain element with index = $index" },
)
}
private fun getCardTypeResolver(index: Int): CardTypesResolver = getWallet(index).scanResponse.cardTypesResolver
}