Updated on 2026-08-14

This commit is contained in:
Tangem 2023-07-20 20:57:39 +08:00
parent 584d99e95e
commit a57d077863
9 changed files with 313 additions and 81 deletions

View file

@ -20,7 +20,7 @@ internal object WalletPreviewData {
val walletTopBarConfig = WalletTopBarConfig(onScanCardClick = {}, onMoreClick = {})
val walletCardContentState = WalletCardState.Content(
id = UUID.randomUUID().toString(),
id = UserWalletId(UUID.randomUUID().toString()),
title = "Wallet 1",
balance = "8923,05 $",
additionalInfo = "3 cards • Seed enabled",
@ -29,7 +29,7 @@ internal object WalletPreviewData {
)
val walletCardLoadingState = WalletCardState.Loading(
id = UUID.randomUUID().toString(),
id = UserWalletId(UUID.randomUUID().toString()),
title = "Wallet 1",
additionalInfo = "3 cards • Seed enabled",
imageResId = R.drawable.ill_businessman_3d,
@ -37,7 +37,7 @@ internal object WalletPreviewData {
)
val walletCardHiddenContentState = WalletCardState.HiddenContent(
id = UUID.randomUUID().toString(),
id = UserWalletId(UUID.randomUUID().toString()),
title = "Wallet 1",
additionalInfo = "3 cards • Seed enabled",
imageResId = R.drawable.ill_businessman_3d,
@ -45,7 +45,7 @@ internal object WalletPreviewData {
)
val walletCardErrorState = WalletCardState.Error(
id = UUID.randomUUID().toString(),
id = UserWalletId(UUID.randomUUID().toString()),
title = "Wallet 1",
additionalInfo = "3 cards • Seed enabled",
imageResId = R.drawable.ill_businessman_3d,

View file

@ -4,6 +4,7 @@ import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.hilt.navigation.compose.hiltViewModel
@ -40,8 +41,9 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation
startDestination = WalletScreens.WALLET.name,
) {
composable(WalletScreens.WALLET.name) {
val viewModel = hiltViewModel<WalletViewModel>()
.apply { router = this@DefaultWalletRouter }
val viewModel = hiltViewModel<WalletViewModel>().apply { router = this@DefaultWalletRouter }
LocalLifecycleOwner.current.lifecycle.addObserver(observer = viewModel)
WalletScreen(state = viewModel.uiState)
}

View file

@ -2,13 +2,14 @@ package com.tangem.feature.wallet.presentation.wallet.state
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.domain.wallets.models.UserWalletId
/** Wallet card state */
@Immutable
internal sealed interface WalletCardState {
/** Id */
val id: String
val id: UserWalletId
/** Title */
val title: String
@ -34,7 +35,7 @@ internal sealed interface WalletCardState {
* @property balance wallet balance
*/
data class Content(
override val id: String,
override val id: UserWalletId,
override val title: String,
override val additionalInfo: String,
override val imageResId: Int?,
@ -52,7 +53,7 @@ internal sealed interface WalletCardState {
* @property onClick lambda be invoked when wallet card is clicked
*/
data class Loading(
override val id: String,
override val id: UserWalletId,
override val title: String,
override val additionalInfo: String,
override val imageResId: Int?,
@ -69,7 +70,7 @@ internal sealed interface WalletCardState {
* @property onClick lambda be invoked when wallet card is clicked
*/
data class HiddenContent(
override val id: String,
override val id: UserWalletId,
override val title: String,
override val additionalInfo: String,
override val imageResId: Int?,
@ -86,7 +87,7 @@ internal sealed interface WalletCardState {
* @property onClick lambda be invoked when wallet card is clicked
*/
data class Error(
override val id: String,
override val id: UserWalletId,
override val title: String,
override val additionalInfo: String,
override val imageResId: Int?,

View file

@ -2,7 +2,9 @@ package com.tangem.feature.wallet.presentation.wallet.state
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
* Wallet screen state holder
@ -26,6 +28,44 @@ internal sealed class WalletStateHolder(
open val bottomSheet: WalletBottomSheetConfig? = null,
) {
fun copySealed(
onBackClick: () -> Unit = this.onBackClick,
topBarConfig: WalletTopBarConfig = this.topBarConfig,
walletsListConfig: WalletsListConfig = this.walletsListConfig,
pullToRefreshConfig: WalletPullToRefreshConfig = this.pullToRefreshConfig,
contentItems: ImmutableList<WalletContentItemState> = this.contentItems,
notifications: ImmutableList<WalletNotification> = this.notifications,
bottomSheet: WalletBottomSheetConfig? = this.bottomSheet,
): WalletStateHolder {
return when (this) {
is MultiCurrencyContent -> this.copy(
onBackClick = onBackClick,
topBarConfig = topBarConfig,
walletsListConfig = walletsListConfig,
pullToRefreshConfig = pullToRefreshConfig,
contentItems = contentItems as ImmutableList<WalletContentItemState.MultiCurrencyItem>,
notifications = notifications,
bottomSheet = bottomSheet,
)
is SingleCurrencyContent -> this.copy(
onBackClick = onBackClick,
topBarConfig = topBarConfig,
walletsListConfig = walletsListConfig,
pullToRefreshConfig = pullToRefreshConfig,
contentItems = contentItems as ImmutableList<WalletContentItemState.SingleCurrencyItem>,
notifications = notifications,
bottomSheet = bottomSheet,
)
is UnlockWalletContent -> this.copy(
onBackClick = onBackClick,
topBarConfig = topBarConfig,
walletsListConfig = walletsListConfig,
pullToRefreshConfig = pullToRefreshConfig,
)
is Loading -> copy(onBackClick = onBackClick)
}
}
/**
* Multi currency wallet content state
*
@ -44,7 +84,7 @@ internal sealed class WalletStateHolder(
override val pullToRefreshConfig: WalletPullToRefreshConfig,
override val contentItems: ImmutableList<WalletContentItemState.MultiCurrencyItem>,
override val notifications: ImmutableList<WalletNotification>,
override val bottomSheet: WalletBottomSheetConfig,
override val bottomSheet: WalletBottomSheetConfig? = null,
val onOrganizeTokensClick: () -> Unit,
) : WalletStateHolder(
onBackClick = onBackClick,
@ -75,7 +115,7 @@ internal sealed class WalletStateHolder(
override val pullToRefreshConfig: WalletPullToRefreshConfig,
override val contentItems: ImmutableList<WalletContentItemState.SingleCurrencyItem>,
override val notifications: ImmutableList<WalletNotification>,
override val bottomSheet: WalletBottomSheetConfig,
override val bottomSheet: WalletBottomSheetConfig? = null,
val buttons: ImmutableList<ActionButtonConfig>,
val marketPriceBlockState: MarketPriceBlockState,
) : WalletStateHolder(
@ -87,4 +127,67 @@ internal sealed class WalletStateHolder(
notifications = notifications,
bottomSheet = bottomSheet,
)
/**
* Unlock wallet content state
*
* @property onBackClick lambda be invoked when back button is clicked
* @property topBarConfig top bar config
* @property walletsListConfig wallets list config
* @property onUnlockWalletsNotificationClick lambda be invoked when unlock wallets notification is clicked
* @property onBottomSheetDismissRequest lambda be invoked when bottom sheet is dismissed
* @property onUnlockClick lambda be invoked when unlock button is clicked
* @property onScanClick lambda be invoked when scan card button is clicked
*/
data class UnlockWalletContent(
override val onBackClick: () -> Unit,
override val topBarConfig: WalletTopBarConfig,
override val walletsListConfig: WalletsListConfig,
override val pullToRefreshConfig: WalletPullToRefreshConfig,
val onUnlockWalletsNotificationClick: () -> Unit,
val onBottomSheetDismissRequest: () -> Unit,
val onUnlockClick: () -> Unit,
val onScanClick: () -> Unit,
) : WalletStateHolder(
onBackClick = onBackClick,
topBarConfig = topBarConfig,
walletsListConfig = walletsListConfig,
pullToRefreshConfig = pullToRefreshConfig,
contentItems = persistentListOf(WalletContentItemState.Loading),
notifications = persistentListOf(WalletNotification.UnlockWallets(onUnlockWalletsNotificationClick)),
bottomSheet = WalletBottomSheetConfig(
isShow = false,
onDismissRequest = onBottomSheetDismissRequest,
content = WalletBottomSheetConfig.BottomSheetContentConfig.UnlockWallets(
onUnlockClick = onUnlockClick,
onScanClick = onScanClick,
),
),
)
/**
* Loading state
*
* @property onBackClick lambda be invoked when back button is clicked
*/
data class Loading(override val onBackClick: () -> Unit) : WalletStateHolder(
onBackClick = onBackClick,
topBarConfig = WalletTopBarConfig(onScanCardClick = {}, onMoreClick = {}),
walletsListConfig = WalletsListConfig(
selectedWalletIndex = 0,
wallets = persistentListOf(
WalletCardState.Loading(
id = UserWalletId(stringValue = ""),
title = "",
additionalInfo = "",
imageResId = null,
),
),
onWalletChange = {},
),
pullToRefreshConfig = WalletPullToRefreshConfig(isRefreshing = false, onRefresh = {}),
contentItems = persistentListOf(WalletContentItemState.Loading),
notifications = persistentListOf(),
bottomSheet = null,
)
}

View file

@ -0,0 +1,109 @@
package com.tangem.feature.wallet.presentation.wallet.state.builder
import com.tangem.common.Provider
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.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver
import com.tangem.feature.wallet.presentation.wallet.state.*
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
internal class WalletStateFactory(
private val routerProvider: Provider<InnerWalletRouter>,
private val onScanCardClick: () -> Unit,
private val onWalletChange: (Int) -> Unit,
private val onRefreshSwipe: () -> Unit,
) {
fun getInitialState(): WalletStateHolder = WalletStateHolder.Loading(onBackClick = ::onBackClick)
fun getContentState(wallets: List<UserWallet>): WalletStateHolder {
val cardTypeResolver = requireNotNull(wallets.firstOrNull()).scanResponse.cardTypesResolver
return if (cardTypeResolver.isMultiwalletAllowed()) {
createMultiCurrencyState(wallets)
} else {
createSingleCurrencyState(wallets)
}
}
private fun createMultiCurrencyState(wallets: List<UserWallet>): WalletStateHolder.MultiCurrencyContent {
return WalletStateHolder.MultiCurrencyContent(
onBackClick = ::onBackClick,
topBarConfig = createTopBarConfig(),
walletsListConfig = createWalletsListConfig(wallets),
pullToRefreshConfig = createPullToRefreshConfig(),
contentItems = persistentListOf(),
notifications = persistentListOf(), // TODO: create notifications
bottomSheet = WalletBottomSheetConfig(
// TODO: check notifications
isShow = false,
onDismissRequest = {},
content = WalletBottomSheetConfig.BottomSheetContentConfig.LikeTangemApp(
onRateTheAppClick = {},
onShareClick = {},
),
),
onOrganizeTokensClick = routerProvider()::openOrganizeTokensScreen,
)
}
private fun createSingleCurrencyState(wallets: List<UserWallet>): WalletStateHolder.SingleCurrencyContent {
return WalletStateHolder.SingleCurrencyContent(
onBackClick = ::onBackClick,
topBarConfig = createTopBarConfig(),
walletsListConfig = createWalletsListConfig(wallets),
pullToRefreshConfig = createPullToRefreshConfig(),
contentItems = persistentListOf(),
notifications = persistentListOf(), // TODO: create notifications
bottomSheet = WalletBottomSheetConfig(
// TODO: check notifications
isShow = false,
onDismissRequest = {},
content = WalletBottomSheetConfig.BottomSheetContentConfig.LikeTangemApp(
onRateTheAppClick = {},
onShareClick = {},
),
),
buttons = WalletPreviewData.singleWalletScreenState.buttons, // TODO: create buttons
// TODO: create market price block
marketPriceBlockState = WalletPreviewData.singleWalletScreenState.marketPriceBlockState,
)
}
private fun onBackClick() = routerProvider().popBackStack()
private fun createTopBarConfig(): WalletTopBarConfig {
return WalletTopBarConfig(
onScanCardClick = onScanCardClick,
onMoreClick = routerProvider()::openDetailsScreen,
)
}
private fun createWalletsListConfig(wallets: List<UserWallet>): WalletsListConfig {
return WalletsListConfig(
selectedWalletIndex = 0,
wallets = wallets.map { wallet ->
WalletCardState.Loading(
id = if (wallet.scanResponse.cardTypesResolver.isMultiwalletAllowed()) { // TODO
UserWalletId("123")
} else {
UserWalletId("321")
},
// TODO: wallet.walletId,
title = wallet.name,
additionalInfo = "", // TODO
imageResId = WalletImageResolver.resolve(cardTypesResolver = wallet.scanResponse.cardTypesResolver),
)
}.toImmutableList(),
onWalletChange = onWalletChange,
)
}
private fun createPullToRefreshConfig(): WalletPullToRefreshConfig {
return WalletPullToRefreshConfig(isRefreshing = false, onRefresh = onRefreshSwipe)
}
}

View file

@ -19,10 +19,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.wallet.state.WalletCardState
import com.tangem.feature.wallet.presentation.wallet.state.WalletsListConfig
import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollector
import kotlinx.coroutines.InternalCoroutinesApi
/**
* Wallets list component
@ -32,7 +30,7 @@ import kotlinx.coroutines.InternalCoroutinesApi
*
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalFoundationApi::class, InternalCoroutinesApi::class)
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState, modifier: Modifier = Modifier) {
val horizontalCardPadding = TangemTheme.dimens.spacing16
@ -45,7 +43,7 @@ internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState),
) {
items(items = config.wallets, key = WalletCardState::id) { state ->
items(items = config.wallets, key = { it.id.stringValue }) { state ->
WalletCard(state = state, modifier = Modifier.width(itemWidth))
}
}

View file

@ -12,21 +12,27 @@ import com.tangem.core.ui.res.TangemTheme
*/
internal fun Modifier.walletContentItemDecoration(currentIndex: Int, lastIndex: Int): Modifier = composed {
val modifierWithHorizontalPadding = this.padding(horizontal = TangemTheme.dimens.spacing16)
when (currentIndex) {
0 -> {
val isSingleItem = currentIndex == 0 && lastIndex == 0
when {
isSingleItem -> {
modifierWithHorizontalPadding
.padding(top = TangemTheme.dimens.spacing14)
.clip(shape = TangemTheme.shapes.roundedCornersXMedium)
}
currentIndex == 0 -> {
modifierWithHorizontalPadding
.padding(top = TangemTheme.dimens.spacing14)
.clip(
RoundedCornerShape(
shape = RoundedCornerShape(
topStart = TangemTheme.dimens.radius16,
topEnd = TangemTheme.dimens.radius16,
),
)
}
lastIndex -> {
currentIndex == lastIndex -> {
modifierWithHorizontalPadding
.clip(
RoundedCornerShape(
shape = RoundedCornerShape(
bottomStart = TangemTheme.dimens.radius16,
bottomEnd = TangemTheme.dimens.radius16,
),

View file

@ -19,6 +19,9 @@ internal class TokenListToWalletStateConverter(
return when (currentState) {
is MultiCurrencyContent -> currentState.updateWithTokenList(value)
is SingleCurrencyContent -> currentState.updateWithTokenList(value)
is WalletStateHolder.Loading,
is WalletStateHolder.UnlockWalletContent,
-> currentState
}
}

View file

@ -1,35 +1,33 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.*
import arrow.core.Either
import com.tangem.common.Provider
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.core.ui.components.transactions.TransactionState
import com.tangem.domain.card.GetAccessCodeSavingStatusUseCase
import com.tangem.domain.card.GetBiometricsStatusUseCase
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.SetAccessCodeRequestPolicyUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder
import com.tangem.feature.wallet.presentation.wallet.state.WalletTopBarConfig
import com.tangem.feature.wallet.presentation.wallet.utils.LoadingItemsProvider.getLoadingMultiCurrencyTokens
import com.tangem.feature.wallet.presentation.wallet.state.*
import com.tangem.feature.wallet.presentation.wallet.state.builder.WalletStateFactory
import com.tangem.feature.wallet.presentation.wallet.utils.TokenListErrorToWalletStateConverter
import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -48,50 +46,66 @@ internal class WalletViewModel @Inject constructor(
private val setAccessCodeRequestPolicyUseCase: SetAccessCodeRequestPolicyUseCase,
private val getAccessCodeSavingStatusUseCase: GetAccessCodeSavingStatusUseCase,
private val getTokenListUseCase: GetTokenListUseCase,
private val getWalletsUseCase: GetWalletsUseCase,
private val scanCardProcessor: ScanCardProcessor,
private val dispatchers: CoroutineDispatcherProvider,
) : ViewModel() {
) : ViewModel(), DefaultLifecycleObserver {
/** Feature router */
var router: InnerWalletRouter by Delegates.notNull()
private val stateFactory = WalletStateFactory(
routerProvider = Provider { router },
onScanCardClick = ::onScanCardClick,
onWalletChange = ::changeWallet,
onRefreshSwipe = ::refreshContent,
)
/** Screen state */
var uiState by mutableStateOf(getInitialState())
var uiState by mutableStateOf(stateFactory.getInitialState())
private set
private var getTokenListJob: Job? = null
set(value) {
field?.cancel()
field = value
override fun onCreate(owner: LifecycleOwner) {
getWalletsUseCase()
.distinctUntilChanged()
.flowWithLifecycle(owner.lifecycle)
.onEach { wallets ->
if (wallets.isEmpty()) return@onEach
uiState = stateFactory.getContentState(wallets = wallets)
updateContentItems()
}
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun updateContentItems() {
getTokenListUseCase(
userWalletId = uiState.walletsListConfig.wallets.get(
index = uiState.walletsListConfig.selectedWalletIndex,
).id,
)
.distinctUntilChanged()
.mapLatest(::updateStateWithTokenListOrError)
.onEach {
uiState = it.copySealed(
pullToRefreshConfig = uiState.pullToRefreshConfig.copy(isRefreshing = getRefreshingStatus()),
)
}
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
}
private fun getRefreshingStatus(): Boolean {
return uiState.contentItems.any { state ->
val isMultiCurrencyItem = state as? WalletContentItemState.MultiCurrencyItem.Token
val isSingleCurrencyItem = state as? WalletContentItemState.SingleCurrencyItem.Transaction
isMultiCurrencyItem?.state is TokenItemState.Loading ||
isSingleCurrencyItem?.state is TransactionState.Loading ||
state is WalletContentItemState.Loading
}
// TODO: [REDACTED_TASK_KEY] Use production data instead of WalletPreviewData
private fun getInitialState(): WalletStateHolder {
val state = WalletPreviewData.multicurrencyWalletScreenState.copy(
onBackClick = ::onBackClick,
topBarConfig = createTopBarConfig(),
walletsListConfig = WalletPreviewData.multicurrencyWalletScreenState.walletsListConfig.copy(
onWalletChange = ::selectWallet,
),
contentItems = getLoadingMultiCurrencyTokens(),
)
val selectedWalletIndex = state.walletsListConfig.selectedWalletIndex
val selectedWalletId = WalletPreviewData.wallets.keys.elementAt(selectedWalletIndex)
launchGetTokenListJob(selectedWalletId)
return state
}
private fun onBackClick() {
router.popBackStack()
}
private fun createTopBarConfig(): WalletTopBarConfig {
return WalletTopBarConfig(
onScanCardClick = ::onScanCardClick,
onMoreClick = { router.openDetailsScreen() },
)
}
private fun onScanCardClick() {
@ -124,12 +138,9 @@ internal class WalletViewModel @Inject constructor(
}
}
// TODO: [REDACTED_TASK_KEY] Use production data instead of WalletPreviewData
private fun selectWallet(index: Int) {
private fun changeWallet(index: Int) {
if (uiState.walletsListConfig.selectedWalletIndex == index) return
Log.i("WalletViewModel", "selectWallet: $index")
uiState = when (val state = uiState) {
is WalletStateHolder.MultiCurrencyContent -> state.copy(
walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index),
@ -137,19 +148,18 @@ internal class WalletViewModel @Inject constructor(
is WalletStateHolder.SingleCurrencyContent -> state.copy(
walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index),
)
is WalletStateHolder.UnlockWalletContent -> state.copy(
walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index),
)
is WalletStateHolder.Loading -> state
}
val selectedWalletId = WalletPreviewData.wallets.keys.elementAt(index)
launchGetTokenListJob(selectedWalletId)
updateContentItems()
}
private fun launchGetTokenListJob(userWalletId: UserWalletId) {
getTokenListJob = getTokenListUseCase(userWalletId)
.distinctUntilChanged()
.mapLatest(::updateStateWithTokenListOrError)
.onEach { uiState = it }
.flowOn(Dispatchers.Default)
.launchIn(viewModelScope)
private fun refreshContent() {
uiState = uiState.copySealed(pullToRefreshConfig = uiState.pullToRefreshConfig.copy(isRefreshing = true))
updateContentItems()
}
private fun updateStateWithTokenListOrError(tokenList: Either<TokenListError, TokenList>): WalletStateHolder {